你将如何在C#中实现“特质”devise模式?

我知道这个特性在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 

现在,我可以满足我所有客户的需求,而不会增加任何额外的费用。 如果我的客户回来后说:“哦,我真的很喜欢这个function,我可以吗?”,我只是更新类定义,包括额外的特质。

你将如何在C#中完成这个任务?

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

(“客户”是指雇用我作为开发人员的字面上的人,而“客户”是指编程类;我的每个客户都有他们想要logging信息的客户)

你可以通过使用标记接口和扩展方法来获得语法。

先决条件:接口需要定义稍后由扩展方法使用的合同。 基本上,接口定义了能够“实现”特征的合同; 理想情况下,添加接口的类应该已经具有接口的所有成员,因此不需要额外的实现。

 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; } } 

然后,如果“trait接口”从IDynamicObjectinheritance,则trait方法可以添加和检索数据:

 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数据,当与dynamic关键字一起使用时,访问附加属性是透明的。

C# 语言 (至less到版本5)不支持Traits。

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

考虑到至less在Scala的意义上,Traits可以被认为是“用代理方法编译的漂亮魔法”(它们影响MRO,这与Ruby中的Mixin不同)。 在C#中,获取这种行为的方法是使用接口和“大量的手动代理方法”(例如组合)。

这个单调乏味的过程可以用一个假设的处理器来完成(可能是通过模板为分部类自动生成代码?),但这不是C#。

快乐的编码。

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

查看CSharpT上的论文(PDF) ,了解他所做的基于单声道编译器的完整描述。

这是一个可以写的东西的例子:

 trait TCircle { public int Radius { get; set; } public int Surface { get { ... } } } trait TColor { ... } class MyCircle { uses { TCircle; TColor } } 

我想指出NRoles ,一个在C#中angular色的实验, angular色特性类似。

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实现RSwitchableRTunable 。 在幕后, Does<R>是没有成员的接口,所以Radio基本上编译成一个空类。 后编译IL重写将RSwitchableRTunable的方法注入到Radio ,然后可以像使用另一个程序RTunable的两个angular色一样使用它:

 var radio = new Radio(); radio.TurnOn(); radio.Seek(42); 

要在重写之前直接使用radio (也就是说,在与Radiotypes声明相同的程序集中),必须使用扩展方法As<R> ():

 radio.As<RSwitchable>().TurnOn(); radio.As<RTunable>().Seek(42); 

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

这实际上是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; } } 

基于Lucero的build议 ,我想出了这个:

 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的面向方面编程的版本。 有些工具可以帮助PostSharp或MS Unity。 如果你想自己动手,使用C#属性的代码注入是一种方法,或作为有限情况下的build议扩展方法。

真的取决于你想得到多么复杂。 如果你正在试图build立一些复杂的东西,我会看看其中的一些工具来帮助你。

我已经使用Traits并坦率地不明白他们为什么存在。 这更像是一个命名空间function,然后是一个OOPfunction。 这是一种封装另一个对象的简单方式,无需编写代码来完成它,或者编写封装对象的名称来使用它。

一旦你明白这一切是有它的。 通过使用封装,在C#中使用特征模式非常容易。

我们来看一个Model基类,为它创build一个Document类,也是一个处理称为Publish

 /// <summary> /// This is our generic base class. /// </summary> public abstract class Model { /// <summary> /// A generic IO operation on models. /// </summary> public void Write(string value) { // blah blah } } /// <summary> /// This is our trait class. /// </summary> public class Publish { private Model _owner; public Publish(Model owner) { this._owner = owner; } public void ChangeStatus(string status) { // write a new status to the model _owner.Write(status); } } /// <summary> /// This is our document class with the trait. /// </summary> public class Document : Model { /// <summary> /// The contained trait. /// </summary> private Publish _publish; /// <summary> /// Public read-only access. /// </summary> public Publish Publish { get { return _publish; } } /// <summary> /// Constructor creates the trait /// </summary> public Document() { this._publish = new Publish(this); } } 

现在一旦你创build了这个设置。 您可以像这样使用Document上的Publish特性。

 Document doc = new Document(); doc.Publish.ChangeStatus("review"); 

Publish特性添加到Model任何派生类是很容易的, Publish特性理解如何像使用真正的特性一样使用Model

那么特质有什么大不了的?

它减less了代码行。 我们来设想一下,C#有一个特征。 上面的代码可能写成这样的东西。

 /// <summary> /// This is our generic base class. /// </summary> public abstract class Model { /// <summary> /// A generic IO operation on models. /// </summary> public void Write(string value) { // blah blah } } /// <summary> /// This is our trait class. /// </summary> public class Publish traits Model { public void ChangeStatus(Model owner, string status) { // write a new status to the model owner.Write(status); } } /// <summary> /// This is our document class with the trait. /// </summary> public class Document : Model uses Publish { } 

现在当我们想要执行trai方法时,代码会这样。

 Document doc = new Document(); doc.ChangeStatus("review"); 

我们不必使用该属性,而特征function会将Document引用添加为ChangeStatus的第一个参数。 除此之外,没有什么更多的特质。

我唯一的问题就是它使用traits类的方法来污染类接口。 在实例化之后,有些语言可以将特征附加到对象上,这进一步使追踪类方法变得更加困难。 在编写代码时,它们也使IDE很难使用自动完成function。

我想从语言特征中获得比这更大的好处,因为我遇到过与对象上的方法碰撞的特征,并且会导致很多重构的痛苦。