C# 如何检测接口和实现类之间的默认参数值不匹配?

C# 如何检测接口和实现类之间的默认参数值不匹配?,c#,interface,compiler-errors,default,C#,Interface,Compiler Errors,Default,我最近有一个丑陋的错误,我的收藏出乎意料地有一个相反的顺序 public class WidgetContainer : IWidgetContainer { // ... public void Add(IWidget widget, int? index = null) { if (!index.HasValue) _collection.Add(widget); else _collec

我最近有一个丑陋的错误,我的收藏出乎意料地有一个相反的顺序

public class WidgetContainer : IWidgetContainer
{
    // ...
    public void Add(IWidget widget, int? index = null)
    {
        if (!index.HasValue)
            _collection.Add(widget);
        else
            _collection.Insert(index.Value, widget);
    }
}
调用代码未指定
索引
。因此,出于所有目的,这段代码应该一直在工作,按顺序插入元素

但事实并非如此

然后我看了一下界面:

public interface IWidgetContainer
{
    void Add(IWidget widget, int? index = 0);
}

调用代码通过接口解析实例,因此使用了
0
而不是
null

没有编译器错误,没有警告-没有。我可以在什么地方启用它们吗

如果没有,我是否可以自动检测并防止此类问题,可能是通过解决方案测试?莫诺,塞西尔,反射都可以接受

调用代码通过接口解析实例,因此使用了
0
而不是
null

嗯,是的。如果调用是
someIWidgetContainerTypedReference.Add(小部件)
那么调用显然将解析为
IWidgetContainerTypedReference.Add

现在,默认参数的工作方式是编译器essential在调用站点将
someIWidgetContainerTypedReference.Add(小部件)
转换为
someIWidgetContainerTypedReference.Add(小部件,0)
。这是这里的关键点,可选参数不是在被调用的方法中“处理”的,而是直接烘焙到调用本身中,因此
WidgetContainer.Add
中指定的可选参数被完全忽略

您的情况非常糟糕,因为您的
WidgetContainer
类实际上是在“破坏”接口契约


最好的解决方案是什么?使您的类完全实现接口,包括可选参数默认值。一个很好的问题是为什么编译器不执行此操作(IMHO它应该执行)。

如果只想检查默认参数是否不同,可以使用以下代码:

var parameter = typeof(WidgetContainer).GetMethod("Add").GetParameters().FirstOrDefault(p => p.Name == "index");
var iparameter = typeof(IWidgetContainer).GetMethod("Add").GetParameters().FirstOrDefault(p => p.Name == "index");
if(!object.Equals(parameter.DefaultValue, iparameter.DefaultValue))
{
    // they are different
}
这也可以在单元测试和生产代码中使用,但我建议在某些测试类中使用它


这可以用一种更通用的方式完成,对您的案例更有用:

void PerformCheck(Type classType, Type interfaceType)
{
    var methods = interfaceType.GetMethods().Where(m => m.GetParameters().Any(p => p.IsOptional));
    foreach(var method in methods)
    {
       var optionalParameters = method.GetParameters().Where(p => p.IsOptional);
        foreach(var parameter in optionalParameters)
        {
            var classParam = classType.GetMethod(method.Name).GetParameters().FirstOrDefault(p => p.Name == parameter.Name);   
            if(!object.Equals(classParam.DefaultValue, parameter.DefaultValue))
            {
                Console.WriteLine("method " + method.Name + " has different defaul values on parameter " + parameter.Name);
            }
        }
    }
}
然后您可以在一些测试应用程序中使用它,只需像这样调用上面的代码:

var types = typeof(WidgetContainer).Assembly.GetTypes().Where(t => t.GetInterfaces().Any());
foreach(var type in types)
{
    var interfaces = type.GetInterfaces();
    foreach(var i in interfaces)
    {
        performCheck(type, i);
    }
}


应用于组件:

Assembly
    .GetExecutingAssembly()
    .GetTypes()
    .Where(t => t.IsClass)
    .Select(GetDefaultParameterValuesMismatch)
    .Where(m => m.Count() > 0);

IEnumerable<(string Interface, string Class, string Method, string Parameter, object InterfaceParameterValue, object ClassParameterValue)>
    GetDefaultParameterValuesMismatch(Type type)
{
    var interfaceParameterValues = type
        .GetTypeInfo()
        .ImplementedInterfaces
        .SelectMany(i => i.GetMethods().Select(m => new { Type = i.Name, m }))
        .SelectMany(t => t.m.GetParameters().Select(p => new
        {
            Type = t.Type,
            Method = t.m.Name,
            Parameter = p.Name,
            p.DefaultValue
        }));

    var classParameterValues = type
        .GetTypeInfo()
        .GetMethods()
        .SelectMany(m => m.GetParameters().Select(p => new
        {
            Type = type.Name,
            Method = m.Name,
            Parameter = p.Name,
            p.DefaultValue
        }));

    return interfaceParameterValues
            .Zip(classParameterValues, (t1, t2) => new { t1, t2 })
            .Where(typePair => !object.Equals(typePair.t1.DefaultValue, (typePair.t2.DefaultValue)))
            .Select(typePair => (Interface: typePair.t1.Type,
                          Class: typePair.t2.Type,
                          Method: typePair.t1.Method,
                          Parameter: typePair.t1.Parameter,
                          InterfaceParameterValue: typePair.t1.DefaultValue,
                          ClassParameterValue: typePair.t2.DefaultValue));
}
汇编
.getExecutionGassembly()
.GetTypes()
.Where(t=>t.IsClass)
.选择(GetDefaultParameterValuesMatch)
其中(m=>m.Count()>0);
数不清
GetDefaultParameterValuesMatch(类型)
{
var interfaceParameterValues=类型
.GetTypeInfo()
.实现的接口
.SelectMany(i=>i.GetMethods().Select(m=>new{Type=i.Name,m}))
.SelectMany(t=>t.m.GetParameters().Select(p=>new
{
类型=t.类型,
方法=t.m.Name,
参数=p.名称,
p、 默认值
}));
var classParameterValues=类型
.GetTypeInfo()
.GetMethods()
.SelectMany(m=>m.GetParameters().Select(p=>new
{
Type=Type.Name,
方法=m.名称,
参数=p.名称,
p、 默认值
}));
返回接口参数值
.Zip(classParameterValues,(t1,t2)=>new{t1,t2})
.Where(typePair=>!object.Equals(typePair.t1.DefaultValue,(typePair.t2.DefaultValue)))
.Select(typePair=>(接口:typePair.t1.Type,
类别:typePair.t2.Type,
方法:typePair.t1.Method,
参数:typePair.t1.Parameter,
InterfaceParameterValue:typePair.t1.DefaultValue,
ClassParameterValue:typePair.t2.DefaultValue);
}

您还可以使用Visual Studio的扩展。在这种情况下,将生成代码为“方法重写不应更改参数默认值”的警告。

不要通过
IWidgetContainer
键入的引用调用
Add
。如果不可行,则需要指定可选参数或对齐两个可选参数。默认值不属于方法签名,因此可以在实现类中忽略它。不过,这并不能解决您的问题。使用具体类型不是一个选项,因为我们使用IoC容器,并同意始终解析接口类型的实例。我控制接口和实现,因此可以使用很多选项。我只是想知道是否有一种方法可以在将来自动检测到这些问题。这可能适用于特定的情况,但在一般情况下,这似乎是老年退休金计划所要求的。您必须为每个方法编写一个带有可选参数的测试并运行它。这只是如何检查这些差异的原始想法。这可以用一种更通用的ofc方式来实现。“一个好问题是为什么编译器不强制执行它(IMHO它应该这样做)。”。在C#spec中:“方法的签名特别不包括[…]可选的类型参数约束”@HimBromBeere除非我弄错了,否则类型参数约束是泛型类型约束,因此引号在这里并不真正相关。无论如何,我不是说编译器没有按照规范的要求去做(这将是一个编译器错误),我是在质疑规范本身。我很确定c#团队选择这种行为有很多很好的理由,但它会导致类似这样的不幸情况。这就是为什么c团队从来没有真正想要实现可选参数的原因……这是
!\。t1.DefaultValue.Equals(uu.t2.DefaultValue)
将在
\uuuu.t1.DefaultValue
null
时引发异常。更好的方法是使用
!object.Equals(u.t1.DefaultValue,u.t2.DefaultValue)
。谢谢。对于未来的读者,我想建议将
\uu
更改为有意义的na