如何强制使用方法';s返回值,单位为C#?

如何强制使用方法';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()”方法的情况下“结束链” 考虑以下代码

我有一个用流利语法编写的软件。方法链有一个明确的“结束”,在此之前,代码中实际上没有做任何有用的事情(想想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));
        }
    }