现有的.NET类型能否添加接口?

25

我下面的示例涉及2个.NET类,它们都包含方法CommonMethod。 我想设计MyMethod,可以接受任何一个类(使用)同时保留NetClassA和NetClassB的公共功能。 Case1本应实现这一点,但是如下所述是非法的。 Case2也可以完成目标,但是INetClassA和INetClassB不存在。 因此,我的问题是是否有一种方法可以在现有的.NET类型上强制实施自定义接口(ICommonNetMethods)(情况3)? 欢迎提供解决方案以解决我的问题。

// Case 1:  Illegal because "where" can only have 1 base class
public void MyMethod<Ttype>(Ttype myClass) where Ttype : NetClassA, NetClassB {}

// Case 2:  Legal to utlize multiple "where" interface types
public void MyMethod<Ttype>(Ttype myClass) where Ttype : INetClassA, INetClassB {}

// Case 3:  For this to work ICommonNetMethods must be added to NetClassA/NetClassB
public void MyMethod<Ttype>(Ttype myClass) where Ttype : ICommonNetMethods {}

NetClassA() { This .NET class has method CommonMethod() }
NetClassB() { This .NET class has method CommonMethod() }

interface ICommonNetMethods { void CommonMethod() }

感谢,aidesigner


你能澄清一下你的问题吗?你所说的“向现有类添加接口”是什么意思? - Paulo Santos
1
这是一个在C#中不可能实现的功能,因为该语言不允许扩展已经声明的类(除非它是partial类型)。想一想:这个想法有多正确?两个不继承自同一类型的对象强制执行“共享”功能是正确的吗?你确定那些.NET方法真的是共通的吗? - as-cii
1
@AS-CII 这并不完全正确。看看 Reflection.Emit 和 Mono.Cecil。在 .NET 中,您最终对任何事物都有绝对的控制。问题更多的是为什么以及如何,并且是否值得。几乎总有更好的方法。 - Andrew T Finnell
1
我完全同意你的观点。我只是想指出,并没有一种“语言方式”来完成这种事情。当然,反射存在,但实际上它只是一个库而不是内置语言功能。最重要的是,正如你所说,这并不一定意味着它是这种情况的正确选择。 - as-cii
7个回答

16

有创造性思考的方法可以解决这个问题。

最显而易见的方法:

适配器模式

你可以先构建一个接口,然后再构建两个适配器分别处理NetClassA和NetClassB。通用代码保持不变,具体实现在适配器中。

即使对于密封类也适用,你不需要从NetClassA或NetClassB进行派生。我想把实现留给你去思考,如果你需要代码实现,请一天后再回来,我会发布代码实现。

其他可行的方法:

扩展方法

和/或者

反射

更多帮助

             =====================
             = ICommonNetMethods =
             =====================
                       | (derive)
         |-------------------------------|
====================            ====================
= NetClassAAdapter =            = NetClassBAdapter =
====================            ====================
         | uses (not derive)             | uses (not derive)
   =============                   =============
   = NetClassA =                   = NetClassB =
   =============                   =============

谢谢!维基链接中有C#代码示例。我想将接口应用于一些WinForms按钮,而不是在表单本身上放置一个自定义类派生自“Button”。我需要将接口应用于按钮,以避免在更高级别的视图界面中直接引用“Button”对象。现在我的表单上有普通按钮,并且它实现的IView要求它实现一些“IButton” getters。具体视图使用非接口setter实现这些IButtons,并在表单构造函数中设置它们,“MyIButton1 = new ButtonToIButtonAdapter(button1);”例如; - HodlDwon
为什么不派生包装类呢?如果方法/属性是通用的,它们将只会传递到实际实现。而“使用”则强制你为每个方法编写空的包装方法。 - Jason Coyne
如果您的代码不负责创建类的实例,那么不要派生包装类的一个原因就是这样。也许它们是从第三方库传递给您的。在这种情况下,您需要手动创建适配器,并必须传入原始类的实例。 - Josh Gallagher
对于ReSharper用户来说,编写适配器的一种快速方法是创建一个包含所需适配类字段的适配器类,然后使用ReSharper的“生成代码”=>“委托成员”。 - Josh Gallagher

10

使用 Func<>

假设有两个类 A 和 B,每个类都有一个函数 Foo(虽然这并不是这个解决方案的要求,观察下面的类 C):

public class A { int Foo() { return 1; } }
public class B { int Foo() { return 2; } }
public class C { int Deviant() { return 3; } }

然后在某个代码片段中,您将编写:

var a = new A();
var b = new B();
var c = new C();
var fs = new Func<int>[] {() => a.Foo(), () => b.Foo(), () => c.Deviant()};

所以要使用这个:

foreach(var func in fs) 
   Console.WriteLine(func());

这将输出:

1
2
3

在C#中,Lambda函数是一个非常重要的技术,也是值得学习的。如果您不熟悉Lambda函数,想要了解更多,请从Microsoft的帮助页面开始。

如果您正在考虑更大的接口,可以考虑适配器模式。如果使用每个对象的具体适配器类进行包装看起来太繁琐了,那么可以再次使用Func<>来解决问题。

public interface ISomeInterface
{
   void f1();
   int f2(string p1);
   ...
}

public class FuncImplementation : ISomeInterface
{
   public Action Func_f1 { get; set; }
   public Func<string,int> Func_f2 { get; set; }
   ...
   public void f1() { Func_f1(); }
   public int f2(string p1) { return Func_f2(p1); }
   ...
}

现在您可以内联创建新的适配器:

var adaptA = new FuncImplementation { Func_f1 = MyF1, Func_f2 = Myf2 };
adaptA.f1();

这是一个非常有创意的想法,使用Lambda函数的方式我之前没有想到过。 - AndrewSwerlick
在你的 FuncImplementation 类中,很遗憾语言不允许你使用委托来满足接口。毕竟,public Action f1 { get; set; } 可以通过 f1() 调用,完全符合接口要求,所以我不知道为什么它不能解决,但他们肯定有他们的理由。 - Mark A. Donohoe

5

除非使用像PostSharp这样的代码编织器(但那是作弊的;-),否则您无法在现有代码上强制实施接口。

相反,考虑以下选项:

  1. 如果您只需要一个接口方法,可以使用委托代替。
  2. 您可以为每个类型创建一个简单的包装类,并在其中实现接口。

5

C# 4.0引入了 dynamic 关键字,允许C#开发人员使用动态编程(一种适用于鸭子类型的替代方案)。使用它,您可以这样定义 MyMethod

public void MyMethod(dynamic myClass)
{
    myClass.CommonMethod();
}

您可以通过以下方式,将 NetClassA 和 NetClassB 的实例传递给 MyMethod

var a = new NetClassA();
var b = new NetClassB();
MyMethod(a);
MyMethod(b);

这种方法的缺点在于没有静态类型检查。如果NetClassA或NetClassB没有一个名为CommonMethod且不接受参数的方法,程序将会编译通过,但在运行时失败。
此外,由于没有关联的接口,不清楚可用的功能和属性。避免在公共面向程序集中使用此方法。

3
我能想到的唯一方法(仅限我当前想到的)是从相关的.NET类中派生,然后将您的接口添加到该实现中。然而,我不认为这是最佳解决方案。
那为什么不简单地检查Ttype方法所在的类型,并根据类型执行您的代码呢?
例如:
public void MyMethod<Ttype>(Ttype myClass)
{

    string className = typeof(Ttype).Name;

    switch (className)
    {
        case "NetClassA":
            // Do stuff
            break;
        case "NetClassB":
            // Do stuff
            break;
        default:
            // Do something if necessary
            break;
     }
}

1
这可能有效,但它最终违背了多态的初衷,导致代码难以阅读和测试能力降低。 - Kai Hartmann

1

感谢大家,我对各种选项印象深刻。首先,我已经开始追求委托选项(使用嵌套类型参数和递归(C#)),并且有一个几乎理想的解决方案。这个线程的第二篇帖子展示了我的确切实现。这种方法尝试通过仅传递NETClassA(SrgsItem)和NetClassB(SrgsElement)的所需函数“Add”,而不是整个类来解决问题。这几乎是完美的,除了C#缺乏“泛型变异”支持会成为阻碍。

至于其他选项,它们都非常有见地。在追求委托线程之后,我将尝试Michael和Andrew提出的适配器/Func方法(将添加评论)。如果您有时间,请关注上面的委托线程,因为它与另一个C#方面有关,这可能有助于理解。


0
截至2022年,C#的最佳实践仍然是将外部类映射为值对象或适配器。对于像我这样的一些人来说,这是一种逻辑开销,我希望能够消除。
C#类型系统是“封闭”的,因为我们无法使用新接口扩展现有类。当然,这可以通过使用“新类型模式”来缓解。
class ExternalClass {
  public string InfoWithDifferentLayoutOrName { get; }
}

interface IMyInterface {
  string Info { get; }
}

record struct ExternalClassExtensionWrapper(ExternalClass Value): IMyInterface {
  public string Info => Value.InfoWithDifferentLayoutOrName;
}

T MyAwesomeInnerFunc<T>(T input) where T: IMyInterface { ... }

但是,从代码设计的角度来看,与值对象映射器相比,这种方法并没有减少代码逻辑,因为您仍然需要编写类似于包装器的东西。唯一的区别在于您是依赖具体布局(VO)还是合同(接口)。野外确实存在着一种强迫症,坚持认为接口带来了更低的耦合性,但在这种特定情况下,我并没有看到任何降低认知负担的地方。

您会喜欢一个特征系统,可以在其他接口上扩展接口。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接