您将如何在 C# 中实现“特征”设计模式?
Posted
技术标签:
【中文标题】您将如何在 C# 中实现“特征”设计模式?【英文标题】:How would you implement a "trait" design-pattern in C#? 【发布时间】:2012-05-30 13:11:21 【问题描述】:我知道 C# 中不存在该功能,但 php 最近添加了一个名为 Traits 的功能,起初我认为这有点傻,直到我开始考虑它。
假设我有一个名为Client
的基类。 Client
有一个名为 Name
的属性。
现在我正在开发一个可供许多不同客户使用的可重复使用的应用程序。所有客户都同意客户应该有一个名称,因此它位于基类中。
现在客户 A 过来说他还需要跟踪客户的体重。客户 B 不需要体重,但他想跟踪身高。客户 C 想要同时跟踪体重和身高。
使用特征,我们可以同时创建 Weight 和 Height 特征特征:
class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight
现在我可以满足所有客户的需求,而无需在课堂上添加任何多余的内容。如果我的客户稍后回来说“哦,我真的很喜欢这个功能,我也可以拥有它吗?”,我只需更新类定义以包含额外的特征。
您将如何在 C# 中完成此操作?
接口在这里不起作用,因为我想要属性和任何关联方法的具体定义,并且我不想为每个版本的类重新实现它们。
(“客户”是指雇佣我作为开发人员的字面意义上的人,而“客户”指的是编程类;我的每个客户都有他们想要记录信息的客户)
【问题讨论】:
嗯,通过使用标记接口和扩展方法,您可以非常完美地模拟 C# 中的特征。 @Lucero 这些不是特征,并且缺乏添加新成员的能力(除其他外)。尽管如此,扩展方法还是很不错的。 @Lucero:这可以添加额外的方法,但是如果我想在客户端对象上存储额外的数据呢? @Mark,那么你需要有一些能力在任意对象上动态存储数据,这不是运行时的特性。我会在这方面的回答中添加一些信息。 Traits 以 默认接口方法 的形式出现在 C# 中。请参阅 this proposal 和 the corresponding issue。 (我会发布一个答案,但我对它的了解还不够,无法发布任何有意义的内容。) 【参考方案1】:您可以使用标记接口和扩展方法获取语法。
先决条件:接口需要定义合约,稍后被扩展方法使用。基本上,接口定义了能够“实现”特征的合同;理想情况下,您添加接口的类应该已经存在接口的所有成员,因此不需要额外的实现。
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
这样使用:
var ca = new ClientA();
ca.IsHeavierThan(10); // OK
ca.IsTallerThan(10); // compiler error
编辑:提出了如何存储额外数据的问题。这也可以通过做一些额外的编码来解决:
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;
然后,如果“特征接口”继承自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
注意:通过实现IDynamicMetaObjectProvider
,该对象甚至允许通过 DLR 公开动态数据,从而在与 dynamic
关键字一起使用时使对附加属性的访问变得透明。
【讨论】:
所以你是说把所有的数据放到基类里,把所有的方法实现放到接口上有钩子的扩展方法里?这是一个奇怪的解决方案,但也许可行。我唯一的不满是你让客户端类承载了很多“自重”(未使用的成员)。通过一些花哨的序列化,它不需要保存到磁盘,但它仍然会消耗内存。 “排序”。我肯定想不出 C# 语言中更好的东西,所以 +1。但是,我不认为这与 Trait 相同。 (Mark 概述了服务器限制。) Err.. 我想使用 C# 属性我只需要为每个派生类实现属性,我就可以在那里存储数据。这有点多余,但我想这也比重新实现所有方法要好。 要完成这个答案,我仍然希望看到你定义一个具体的成员变量(我看到的只是属性)。我不确定您是否打算让我在Client
中定义它们,或者根据需要在 ClientB
和 ClientC
中重新定义它们多次。
@Mark,查看我对动态数据存储的更新(实现序列化留给读者作为练习;))。由于接口无法为字段定义契约,因此您不能将字段用作“特征”的一部分,但属性当然可以是读写的!我并不是说 C# 有特征,而是说扩展方法可以作为接口的可重用代码块,因此不需要重新实现方法;当然,代码必须在界面上提供所有需要的成员。【参考方案2】:
可以使用默认接口方法在 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;
ClientA
和 ClientB
具有这些特征,但没有实现它们。 ClientC 仅实现 IClientHeight
并返回不同的数字,在本例中为 16:
class ClientA : Client, IClientWeight
class ClientB : Client, IClientHeight
class ClientC : Client, IClientWeight, IClientHeight
public int getHeight()=>16;
通过接口在ClientB
中调用getHeight()
时,调用的是默认实现。 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();
This SharpLab.io example 显示了此示例生成的代码
PHP overview on traits 中描述的许多特征特性都可以使用默认接口方法轻松实现。可以组合特征(接口)。也可以定义 abstract 方法来强制类实现某些要求。
假设我们希望我们的特征具有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
客户现在已经实现getHeight()
或getWeight()
方法,但不需要了解say
方法的任何信息。
这提供了一种更简洁的装饰方式
SharpLab.io link 用于此示例。
【讨论】:
您需要将其转换为接口类型这一事实似乎使代码更加冗长。你知道它设计成这样的原因吗? @Barsonax 从docs 看来,实现的主要原因是 API 开发以及与 Swift 和 android 的向后兼容性和互操作性,而不是作为特征/混合的语言功能。如果您正在寻找 mixins/traits/多重继承风格的语言特性,我完全同意转换到界面是一件令人烦恼的事情。耻辱。 @MemeDeveloper 和 Java 中的那些特性 用于特征和混合以及版本控制。what's new
页面只是一个简短的描述,不包含原因。您可以在设计会议中的 CSharplang Github 存储库中找到这些内容。 AndroidSDK 使用 DIM 来实现特征,现在 C# 也是如此。 OTOH,Android SDK 互操作性可能是此功能最重要的动机
在我(语言架构外行)看来,在 C# 中不需要任何重大问题来支持这一点。当然,编译器可以像部分类一样处理 - 即如果同一事物有多个定义,编译器可能会出错。看起来应该非常简单,并且会让我的工作日更有效率。无论如何,我想我可以和 Fody 或类似的东西一起工作。我只是想保持它最小化和干燥,并且经常发现自己不遗余力地绕过 C# 中的这个限制。
必须通过显式接口引用访问继承的“特征”实现的原因之一是避免潜在的diamond problem - 多个基本接口/特征可能会暴露相同的方法签名。
【参考方案3】:
C# 语言(至少到版本 5)不支持 Traits。
但是,Scala 具有 Traits,并且 Scala 在 JVM(和 CLR)上运行。因此,这不是运行时的问题,而只是语言的问题。
考虑到 Traits,至少在 Scala 意义上,可以被认为是“在代理方法中编译的神奇魔法”(它们确实不影响 MRO,这与 Ruby 中的 Mixins 不同)。在 C# 中,获得这种行为的方法是使用接口和“大量手动代理方法”(例如组合)。
这个繁琐的过程可以用一个假设的处理器来完成(也许是通过模板为部分类自动生成代码?),但这不是 C#。
编码愉快。
【讨论】:
我不确定这是什么答案。您是否建议我应该拼凑一些东西来预处理我的 C# 代码? @Mark 否。我是 1) 建议 C#,这种语言,不能支持它(尽管可能使用动态代理?这种魔法水平超出了我的能力。) 2) Traits 不会影响 MRO并且可以“手工模拟”;也就是说,可以将 Trait 扁平化到它所混入的每个 Class 中,就像 Composition。 @Mark Ahh,方法解析顺序。也就是说,Traits(同样,在 Scala 意义上仍然基于 Single Inheritance 运行时)实际上并不影响类层次结构。 [虚拟] 调度表中没有添加“特征类”。 Traits 中的方法/属性被复制(在完成期间)到相应的类中。这是 Scala 中使用的一些papers about traits。 Ordersky 提出 Traits 可以在 SI 运行时中使用,这就是它们在编译时被“嵌入”的原因。 @Mark 这不同于像 Ruby 这样的语言,它将“mixin”类型(一种特征形式)注入 MRO(这是一种交替类层次结构的形式,但具有控制和限制)。 我很犹豫是否支持你,因为你还没有为我提供任何具体的东西,只是谈了很多关于其他语言的事情。我试图弄清楚如何从 Scala 中借用其中的一些想法……但这都是该语言的内置内容。它是如何转移的?【参考方案4】:我想指出NRoles,一个在C#中使用roles的实验,其中roles类似于traits。
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>
类Radio
实现RSwitchable
和RTunable
。在幕后,Does<R>
是一个没有成员的接口,所以基本上Radio
编译为一个空类。编译后的 IL 重写将 RSwitchable
和 RTunable
的方法注入到 Radio
中,然后可以像真的从两个 roles (来自另一个程序集)一样使用它:
var radio = new Radio();
radio.TurnOn();
radio.Seek(42);
要在重写发生之前直接使用radio
(即在声明Radio
类型的同一程序集中),您必须求助于扩展方法As<R>
():
radio.As<RSwitchable>().TurnOn();
radio.As<RTunable>().Seek(42);
因为编译器不允许在Radio
类上直接调用TurnOn
或Seek
。
【讨论】:
【参考方案5】:有一个由伯尔尼大学(瑞士)软件组合小组的 Stefan Reichart 开发的学术项目,它提供了 C# 语言的 traits 的真正实现。
查看the paper (PDF) on CSharpT,了解他所做工作的完整描述,基于单声道编译器。
以下是可以编写的示例:
trait TCircle
public int Radius get; set;
public int Surface get ...
trait TColor ...
class MyCircle
uses TCircle; TColor
【讨论】:
【参考方案6】:这确实是对 Lucero 答案的建议扩展,其中所有存储都在基类中。
为此使用依赖属性怎么样?
当您拥有许多并非总是由每个后代设置的属性时,这将使客户端类在运行时变得轻量级。这是因为值存储在静态成员中。
using System.Windows;
public class Client : DependencyObject
public string Name get; set;
public Client(string name)
Name = name;
//add to descendant to use
//public double Weight
//
// get return (double)GetValue(WeightProperty);
// set SetValue(WeightProperty, value);
//
public static readonly DependencyProperty WeightProperty =
DependencyProperty.Register("Weight", typeof(double), typeof(Client), new PropertyMetadata());
//add to descendant to use
//public double Height
//
// get return (double)GetValue(HeightProperty);
// set SetValue(HeightProperty, value);
//
public static readonly DependencyProperty HeightProperty =
DependencyProperty.Register("Height", typeof(double), typeof(Client), new PropertyMetadata());
public interface IWeight
double Weight get; set;
public interface IHeight
double Height get; set;
public class ClientA : Client, IWeight
public double Weight
get return (double)GetValue(WeightProperty);
set SetValue(WeightProperty, value);
public ClientA(string name, double weight)
: base(name)
Weight = weight;
public class ClientB : Client, IHeight
public double Height
get return (double)GetValue(HeightProperty);
set SetValue(HeightProperty, value);
public ClientB(string name, double height)
: base(name)
Height = height;
public class ClientC : Client, IHeight, IWeight
public double Height
get return (double)GetValue(HeightProperty);
set SetValue(HeightProperty, value);
public double Weight
get return (double)GetValue(WeightProperty);
set SetValue(WeightProperty, value);
public ClientC(string name, double weight, double height)
: base(name)
Weight = weight;
Height = height;
public static class ClientExt
public static double HeightInches(this IHeight client)
return client.Height * 39.3700787;
public static double WeightPounds(this IWeight client)
return client.Weight * 2.20462262;
【讨论】:
我们为什么要在这里使用 WPF 类?【参考方案7】:在what Lucero suggested 的基础上,我想出了这个:
internal class Program
private static void Main(string[] args)
var a = new ClientA("Adam", 68);
var b = new ClientB("Bob", 1.75);
var c = new ClientC("Cheryl", 54.4, 1.65);
Console.WriteLine("0 is 1:0.0 lbs.", a.Name, a.WeightPounds());
Console.WriteLine("0 is 1:0.0 inches tall.", b.Name, b.HeightInches());
Console.WriteLine("0 is 1:0.0 lbs and 2:0.0 inches.", c.Name, c.WeightPounds(), c.HeightInches());
Console.ReadLine();
public class Client
public string Name get; set;
public Client(string name)
Name = name;
public interface IWeight
double Weight get; set;
public interface IHeight
double Height get; set;
public class ClientA : Client, IWeight
public double Weight get; set;
public ClientA(string name, double weight) : base(name)
Weight = weight;
public class ClientB : Client, IHeight
public double Height get; set;
public ClientB(string name, double height) : base(name)
Height = height;
public class ClientC : Client, IWeight, IHeight
public double Weight get; set;
public double Height get; set;
public ClientC(string name, double weight, double height) : base(name)
Weight = weight;
Height = height;
public static class ClientExt
public static double HeightInches(this IHeight client)
return client.Height * 39.3700787;
public static double WeightPounds(this IWeight client)
return client.Weight * 2.20462262;
输出:
Adam is 149.9 lbs.
Bob is 68.9 inches tall.
Cheryl is 119.9 lbs and 65.0 inches.
虽然没有我想的那么好,但也不算太差。
【讨论】:
仍然没有 PHP 做的那么高效。【参考方案8】:这听起来像是 PHP 版本的面向方面编程。在某些情况下,有一些工具可以提供帮助,例如 PostSharp 或 MS Unity。如果你想自己动手,使用 C# 属性的代码注入是一种方法,或者作为有限情况的建议扩展方法。
真的取决于你想要变得多复杂。如果您正在尝试构建复杂的东西,我会考虑其中一些工具来提供帮助。
【讨论】:
AoP/PostSharp/Unity 是否允许添加成为 static 类型系统一部分的新成员? (我有限的 AoP 经验只是注释切点和类似的......) PostSharp 重写了 IL 代码,应该可以做到,是的。 是的,我相信是这样,通过成员/接口介绍的方面(如前所述,在 IL 级别)。我的经验也有限,但我没有太多实际机会深入研究这种方法。以上是关于您将如何在 C# 中实现“特征”设计模式?的主要内容,如果未能解决你的问题,请参考以下文章