如何强制使用方法';s返回值,单位为C#?
我有一个用流利语法编写的软件。方法链有一个明确的“结束”,在此之前,代码中实际上没有做任何有用的事情(想想NBuilder,或者Linq-to-SQL的查询生成,在我们使用ToList()迭代对象之前,不会实际命中数据库) 我遇到的问题是,其他开发人员对代码的正确使用感到困惑。他们忽略了调用“结束”方法(因此从来没有真正“做任何事情”) 我感兴趣的是强制使用我的一些方法的返回值,这样我们就永远无法在不调用实际执行工作的“Finalize()”或“Save()”方法的情况下“结束链” 考虑以下代码:如何强制使用方法';s返回值,单位为C#?,c#,compiler-construction,attributes,lazy-loading,return-value,C#,Compiler Construction,Attributes,Lazy Loading,Return Value,我有一个用流利语法编写的软件。方法链有一个明确的“结束”,在此之前,代码中实际上没有做任何有用的事情(想想NBuilder,或者Linq-to-SQL的查询生成,在我们使用ToList()迭代对象之前,不会实际命中数据库) 我遇到的问题是,其他开发人员对代码的正确使用感到困惑。他们忽略了调用“结束”方法(因此从来没有真正“做任何事情”) 我感兴趣的是强制使用我的一些方法的返回值,这样我们就永远无法在不调用实际执行工作的“Finalize()”或“Save()”方法的情况下“结束链” 考虑以下代码
//The "factory" class the user will be dealing with
public class FluentClass
{
//The entry point for this software
public IntermediateClass<T> Init<T>()
{
return new IntermediateClass<T>();
}
}
//The class that actually does the work
public class IntermediateClass<T>
{
private List<T> _values;
//The user cannot call this constructor
internal IntermediateClass<T>()
{
_values = new List<T>();
}
//Once generated, they can call "setup" methods such as this
public IntermediateClass<T> With(T value)
{
var instance = new IntermediateClass<T>() { _values = _values };
instance._values.Add(value);
return instance;
}
//Picture "lazy loading" - you have to call this method to
//actually do anything worthwhile
public void Save()
{
var itemCount = _values.Count();
. . . //save to database, write a log, do some real work
}
}
//用户将要处理的“工厂”类
公共课FluentClass
{
//此软件的入口点
公共类初始化()
{
返回新的中间类();
}
}
//真正做这项工作的班级
公营中级班
{
私有列表值;
//用户无法调用此构造函数
内部中间类()
{
_值=新列表();
}
//一旦生成,他们就可以调用“setup”方法,例如
具有(T值)的公共中间类
{
var instance=newintermediateClass(){u values=\u values};
实例._values.Add(value);
返回实例;
}
//图片“延迟加载”-您必须调用此方法
//做任何值得做的事
公共作废保存()
{
var itemCount=_values.Count();
…//保存到数据库,写日志,做一些实际工作
}
}
如您所见,此代码的正确用法如下:
new FluentClass().Init<int>().With(-1).With(300).With(42).Save();
public IntermediateClass<T> Init<T>()
{
ValidateUsage(Assembly.GetCallingAssembly());
return new IntermediateClass<T>();
}
void ValidateUsage(Assembly assembly)
{
// 1) Use Mono.Cecil to inspect the codebase inside the assembly
var assemblyLocation = assembly.CodeBase.Replace("file:///", "");
var monoCecilAssembly = AssemblyFactory.GetAssembly(assemblyLocation);
// 2) Retrieve the list of Instructions in the calling method
var methods = monoCecilAssembly.Modules...Types...Methods...Instructions
// (It's a little more complicated than that...
// if anybody would like more specific information on how I got this,
// let me know... I just didn't want to clutter up this post)
// 3) Those instructions refer to OpCodes and Operands....
// Defining "invalid method" as a method that calls "Init" but not "Save"
var methodCallingInit = method.Body.Instructions.Any
(instruction => instruction.OpCode.Name.Equals("callvirt")
&& instruction.Operand is IMethodReference
&& instruction.Operand.ToString.Equals(INITMETHODSIGNATURE);
var methodNotCallingSave = !method.Body.Instructions.Any
(instruction => instruction.OpCode.Name.Equals("callvirt")
&& instruction.Operand is IMethodReference
&& instruction.Operand.ToString.Equals(SAVEMETHODSIGNATURE);
var methodInvalid = methodCallingInit && methodNotCallingSave;
// Note: this is partially pseudocode;
// It doesn't 100% faithfully represent either Mono.Cecil's syntax or my own
// There are actually a lot of annoying casts involved, omitted for sanity
// 4) Obviously, if the method is invalid, throw
if (methodInvalid)
{
throw new Exception(String.Format("Bad developer! BAD! {0}", method.Name));
}
}
new FluentClass().Init().With(-1).With(300).With(42.Save();
问题是人们正以这种方式使用它(认为它达到了与上述相同的效果):
newfluentclass().Init().With(-1).With(300).With(42);
这个问题是如此普遍,以至于有一次,出于完全善意,另一个开发人员实际上更改了“Init”方法的名称,以表明该方法正在进行软件的“实际工作”
像这样的逻辑错误很难发现,当然,它是编译的,因为用返回值调用方法并“假装”它返回void是完全可以接受的。Visual Studio不在乎您是否这样做;您的软件仍将编译并运行(尽管在某些情况下,我相信它会发出警告)。当然,这是一个很好的特性。想象一下一个简单的“InsertToDatabase”方法,它以整数形式返回新行的ID——很容易看出,有些情况下我们需要该ID,有些情况下我们可以不需要它
在这个软件中,绝对没有任何理由回避方法链末尾的“保存”功能。这是一个非常专业的实用程序,唯一的收益来自最后一步
我希望某人的软件在编译器级别失败,如果他们调用“With()”而不是“Save()”
用传统的方法来说,这似乎是一项不可能完成的任务——但这就是我来找你们的原因。是否有一个属性可以用来防止方法被“强制转换为无效”或类似的情况
注意:实现这一目标的另一种方法是编写一套单元测试来执行这一规则,并使用类似于将它们绑定到编译器的东西。这是一个可以接受的解决方案,但我希望能有更优雅的解决方案。我不知道有什么方法可以在编译器级别强制实现这一点。对于实现了
IDisposable
但实际上不可执行的对象,通常也会请求它
但是,一个可能有帮助的选项是,如果从未调用Save()
,只在DEBUG中设置类,使其具有记录/抛出/等的终结器。这可以帮助您在调试时发现这些运行时问题,而不是依赖于搜索代码等
但是,请确保在发布模式下不使用此选项,因为添加不必要的终结器会对GC性能造成很大的影响,这将导致性能开销。您可能需要特定的方法来使用如下回调:
new FluentClass().Init<int>(x =>
{
x.Save(y =>
{
y.With(-1),
y.With(300)
});
});
new FluentClass().Init(x=>
{
x、 保存(y=>
{
y、 带(-1),
y、 有(300)
});
});
with方法返回一些特定的对象,获取该对象的唯一方法是调用x.Save(),它本身有一个回调,允许您设置不确定数量的with语句。因此init采用如下方式:
public T Init<T>(Func<MyInitInputType, MySaveResultType> initSetup)
public T Init(Func initSetup)
在调试模式下,除了实现IDisposable之外,您还可以设置一个计时器,如果没有调用resultmethod,则该计时器将在1秒后引发异常。如果使用Init
和不返回FluentClass
类型的对象,该怎么办?让它们返回,例如,UninitializedFluentClass
,它包装了一个FluentClass
对象。然后在UnitializedFluentClass
对象上调用.Save(0
),在包装的FluentClass
对象上调用它并返回它。如果他们不调用Save
,他们就不会得到FluentClass
对象。使用out
参数!必须使用所有out
编辑:我不确定它是否会有帮助,尽管。。。
这将打破流畅的语法。我可以想出三种解决方案,但并不理想
AIUI您需要的是一个函数,当临时变量超出作用域时(如中,当它可用于垃圾收集,但可能在一段时间内不会被垃圾收集时)调用该函数。(请参阅:)这个假设函数会说“如果您构建了
public IntermediateClass<T> Init<T>()
{
ValidateUsage(Assembly.GetCallingAssembly());
return new IntermediateClass<T>();
}
void ValidateUsage(Assembly assembly)
{
// 1) Use Mono.Cecil to inspect the codebase inside the assembly
var assemblyLocation = assembly.CodeBase.Replace("file:///", "");
var monoCecilAssembly = AssemblyFactory.GetAssembly(assemblyLocation);
// 2) Retrieve the list of Instructions in the calling method
var methods = monoCecilAssembly.Modules...Types...Methods...Instructions
// (It's a little more complicated than that...
// if anybody would like more specific information on how I got this,
// let me know... I just didn't want to clutter up this post)
// 3) Those instructions refer to OpCodes and Operands....
// Defining "invalid method" as a method that calls "Init" but not "Save"
var methodCallingInit = method.Body.Instructions.Any
(instruction => instruction.OpCode.Name.Equals("callvirt")
&& instruction.Operand is IMethodReference
&& instruction.Operand.ToString.Equals(INITMETHODSIGNATURE);
var methodNotCallingSave = !method.Body.Instructions.Any
(instruction => instruction.OpCode.Name.Equals("callvirt")
&& instruction.Operand is IMethodReference
&& instruction.Operand.ToString.Equals(SAVEMETHODSIGNATURE);
var methodInvalid = methodCallingInit && methodNotCallingSave;
// Note: this is partially pseudocode;
// It doesn't 100% faithfully represent either Mono.Cecil's syntax or my own
// There are actually a lot of annoying casts involved, omitted for sanity
// 4) Obviously, if the method is invalid, throw
if (methodInvalid)
{
throw new Exception(String.Format("Bad developer! BAD! {0}", method.Name));
}
}