你会如何在C#中实现"特质"设计模式?

mpe*_*pen 48 c# code-reuse design-patterns traits default-interface-member

我知道这个功能在C#中不存在,但PHP最近添加了一个名为Traits的功能,我认为这个功能起初有点傻,直到我开始考虑它.

假设我有一个名为的基类Client.Client有一个叫做的属性Name.

现在我正在开发一个可供许多不同客户使用的可重用应用程序.所有客户都同意客户应该有一个名字,因此它属于基类.

现在客户A出现并表示他还需要跟踪客户的权重.客户B不需要重量,但他想跟踪高度.客户C想要跟踪重量和高度.

有了特征,我们可以制作权重和高度特征:

class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight
Run Code Online (Sandbox Code Playgroud)

现在,我可以满足所有客户的需求,而无需在课堂上添加任何额外的毛病.如果我的客户稍后回来并说"哦,我真的很喜欢这个功能,我也可以拥有它吗?",我只是更新了类定义以包含额外的特性.

你会如何在C#中实现这一目标?

接口在这里不起作用,因为我想要对属性和任何相关方法的具体定义,我不想为每个版本的类重新实现它们.

("客户",我指的是雇用我作为开发人员的文字人员,而"客户"我指的是编程课程;我的每个客户都有他们想要记录信息的客户)

Luc*_*ero 54

您可以使用标记接口和扩展方法来获取语法.

先决条件:接口需要定义后面由扩展方法使用的合同.基本上,界面定义了能够"实现"特征的合同; 理想情况下,添加接口的类应该已经存在接口的所有成员,因此不需要其他实现.

public class Client {
  public double Weight { get; }

  public double Height { get; }
}

public interface TClientWeight {
  double Weight { get; }
}

public interface TClientHeight {
  double Height { get; }
}

public class ClientA: Client, TClientWeight { }

public class ClientB: Client, TClientHeight { }

public class ClientC: Client, TClientWeight, TClientHeight { }

public static class TClientWeightMethods {
  public static bool IsHeavierThan(this TClientWeight client, double weight) {
    return client.Weight > weight;
  }
  // add more methods as you see fit
}

public static class TClientHeightMethods {
  public static bool IsTallerThan(this TClientHeight client, double height) {
    return client.Height > height;
  }
  // add more methods as you see fit
}
Run Code Online (Sandbox Code Playgroud)

使用这样:

var ca = new ClientA();
ca.IsHeavierThan(10); // OK
ca.IsTallerThan(10); // compiler error
Run Code Online (Sandbox Code Playgroud)

编辑:提出了如何存储其他数据的问题.这也可以通过做一些额外的编码来解决:

public interface IDynamicObject {
  bool TryGetAttribute(string key, out object value);
  void SetAttribute(string key, object value);
  // void RemoveAttribute(string key)
}

public class DynamicObject: IDynamicObject {
  private readonly Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);

  bool IDynamicObject.TryGetAttribute(string key, out object value) {
    return data.TryGet(key, out value);
  }

  void IDynamicObject.SetAttribute(string key, object value) {
    data[key] = value;
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,如果"特征接口"继承自IDynamicObject以下特征,那么特征方法可以添加和检索数据:

public class Client: DynamicObject { /* implementation see above */ }

public interface TClientWeight, IDynamicObject {
  double Weight { get; }
}

public class ClientA: Client, TClientWeight { }

public static class TClientWeightMethods {
  public static bool HasWeightChanged(this TClientWeight client) {
    object oldWeight;
    bool result = client.TryGetAttribute("oldWeight", out oldWeight) && client.Weight.Equals(oldWeight);
    client.SetAttribute("oldWeight", client.Weight);
    return result;
  }
  // add more methods as you see fit
}
Run Code Online (Sandbox Code Playgroud)

注意:通过实现IDynamicMetaObjectProvider,对象甚至允许通过DLR公开动态数据,使得在与dynamic关键字一起使用时,对附加属性的访问是透明的.

  • 所以你说的是把所有数据都放在基类中,所有的方法实现都是在接口上有钩子的扩展方法中?这是一个奇怪的解决方案,但也许是可行的.我唯一的好处就是你让客户课程带来了很多"自重"(未使用的成员).通过一些花哨的序列化,它不需要保存到磁盘,但它仍然消耗内存. (5认同)
  • “有点”。我确实想不出 C# 语言中更好的东西,所以+1。然而,我并不认为它与特质具有相同的地位。(Mark 概述了服务器限制。) (2认同)

Pan*_*vos 11

可以使用默认接口方法在 C# 8 中实现特征。由于这个原因,Java 8 也引入了默认接口方法。

使用 C# 8,您几乎可以编写出您在问题中提出的内容。这些特征由 IClientWeight 和 IClientHeight 接口实现,这些接口为其方法提供了默认实现。在这种情况下,它们只返回 0:

public interface IClientWeight
{
    int getWeight()=>0;
}

public interface IClientHeight
{
    int getHeight()=>0;
}

public class Client
{
    public String Name {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

ClientAClientB具有特征但不实施它们。ClientC 仅实现IClientHeight并返回不同的数字,在本例中为 16 :

class ClientA : Client, IClientWeight{}
class ClientB : Client, IClientHeight{}
class ClientC : Client, IClientWeight, IClientHeight
{
    public int getHeight()=>16;
}
Run Code Online (Sandbox Code Playgroud)

当通过接口getHeight()调用时,调用ClientB默认实现。getHeight()只能通过接口调用。

ClientC 实现了 IClientHeight 接口,因此它自己的方法被调用。该方法可通过类本身获得。

public class C {
    public void M() {        
        //Accessed through the interface
        IClientHeight clientB = new ClientB();        
        clientB.getHeight();

        //Accessed directly or through the class
        var clientC = new ClientC();        
        clientC.getHeight();
    }
}
Run Code Online (Sandbox Code Playgroud)

这个 SharpLab.io 示例显示了从这个示例生成的代码

PHP 特性概述中描述的许多特性特性可以使用默认接口方法轻松实现。特征(接口)可以组合。也可以定义抽象方法来强制类实现某些要求。

比方说,我们希望我们的性状有sayHeight()sayWeight()返回一个字符串与身高或体重的方法。他们需要某种方式来强制展示类(从 PHP 指南中窃取的术语)来实现返回身高和体重的方法:

public interface IClientWeight
{
    abstract int getWeight();
    String sayWeight()=>getWeight().ToString();
}

public interface IClientHeight
{
    abstract int getHeight();
    String sayHeight()=>getHeight().ToString();
}

//Combines both traits
public interface IClientBoth:IClientHeight,IClientWeight{}
Run Code Online (Sandbox Code Playgroud)

客户端现在必须实现 thetgetHeight()getWeight()方法,但不需要了解有关这些say方法的任何信息。

这提供了一种更清洁的装饰方式

此示例的SharpLab.io 链接

  • 您需要将其强制转换为接口类型这一事实似乎使代码变得更加冗长。你知道它设计成这样的原因吗? (6认同)
  • @Barsonax从[文档](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#default-interface-methods)看来,实施的主要原因是API开发以及与 Swift 和 Android 的向后兼容性和互操作,而不是作为 Traits / mixins 的语言功能。我完全同意,如果您正在寻找 mixins/traits/多重继承风格的语言功能,那么转换到接口会很麻烦。耻辱。 (4认同)
  • @MemeDeveloper 和 Java 中的这些功能*用于*用于特征、混合和版本控制。“新增内容”页面只是一个简短的描述,不包含原因。您可以在设计会议的 CSharplang Github 存储库中找到这些内容。AndroidSDK 使用 DIM 来实现特征,现在 C# 也是如此。OTOH,Android SDK 互操作性可能是此功能最重要的动机 (2认同)
  • 在我(一个语言架构外行)看来,C# 中不需要任何重大问题来支持这一点。当然,编译器可以处理有点像部分类 - 即,如果同一事物有多个定义,编译器可能会出错。看起来应该非常简单,并且会让我的工作更有效率。无论如何,我想我可以找到一些与福迪或类似的人一起工作的东西。我只是喜欢保持最小和干燥,并且经常发现自己在 C# 中竭尽全力来解决这个限制。 (2认同)

小智 8

C#语言(至少到版本5)不支持Traits.

但是,Scala在JVM(和CLR)上运行Traits和Scala.因此,这不仅仅是运行时的问题,而只是语言问题.

考虑到Traits,至少在Scala意义上,可以被认为是"在代理方法中编译的非常神奇"(它们不会影响MRO,这与Ruby中的Mixins不同).在C#中,获得此行为的方法是使用接口和"大量手动代理方法"(例如组合).

这个繁琐的过程可以用一个假设的处理器完成(也许通过模板自动生成部分类的代码?),但这不是C#.

快乐的编码.

  • 我对你赞不绝口,因为你没有提供任何具体的东西,只是很多关于其他语言的讨论.我试图弄清楚如何从Scala中借用一些这些想法......但这些都是内置于语言中的.它是如何转移的? (3认同)
  • @Mark Ahh,方法解析顺序。也就是说,特征(同样,在 Scala 意义上仍然基于单继承运行时)实际上并不影响类层次结构。[虚拟] 调度表中没有添加“特征类”。Traits 中的方法/属性被复制(在完成期间)到相应的类中。这里有一些 [关于特性的论文](http://scg.unibe.ch/research/traits),如在 Scala 中使用的。Ordersky 介绍了 Traits 可以在 SI 运行时中使用,这就是它们在编译时“烘焙”的原因。 (2认同)

Pie*_*aud 7

有一个学术项目,由伯尔尼大学(瑞士)的软件组合小组的Stefan Reichart开发,它提供了对C#语言特性的真实实现.

在单片机编译器的基础上,查看CSharpT的论文(PDF),了解他所做的全部描述.

以下是可以编写的示例:

trait TCircle
{
    public int Radius { get; set; }
    public int Surface { get { ... } }
}

trait TColor { ... }

class MyCircle
{
    uses { TCircle; TColor }
}
Run Code Online (Sandbox Code Playgroud)


Pie*_*aud 6

我想指出NRoles,用实验的角色在C#中,其中的角色类似的特征.

NRoles使用后编译器重写IL并将方法注入类中.这允许你编写这样的代码:

public class RSwitchable : Role
{
    private bool on = false;
    public void TurnOn() { on = true; }
    public void TurnOff() { on = false; }
    public bool IsOn { get { return on; } }
    public bool IsOff { get { return !on; } }
}

public class RTunable : Role
{
    public int Channel { get; private set; }
    public void Seek(int step) { Channel += step; }
}

public class Radio : Does<RSwitchable>, Does<RTunable> { }
Run Code Online (Sandbox Code Playgroud)

Radio实现RSwitchableRTunable.在幕后,Does<R>是一个没有成员的界面,所以基本上Radio编译成一个空类.编译后的IL重写注入的方法RSwitchableRTunableRadio,因为如果真的来自两个来源的,其然后可用于角色(从另一个组件):

var radio = new Radio();
radio.TurnOn();
radio.Seek(42);
Run Code Online (Sandbox Code Playgroud)

radio在重写之前直接使用(即,在Radio声明类型的同一程序集中),您必须求助于扩展方法As<R>():

radio.As<RSwitchable>().TurnOn();
radio.As<RTunable>().Seek(42);
Run Code Online (Sandbox Code Playgroud)

因为编译器不允许调用TurnOnSeek直接在Radio类上.