您将如何在 C# 中实现“特质”设计模式?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/10729230/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How would you implement a "trait" design-pattern in C#?
提问by mpen
I know the feature doesn't exist in C#, but PHP recently added a feature called Traitswhich I thought was a bit silly at first until I started thinking about it.
我知道 C# 中不存在该功能,但 PHP 最近添加了一个名为Traits的功能,起初我认为它有点傻,直到我开始考虑它。
Say I have a base class called Client. Clienthas a single property called Name.
假设我有一个名为Client. Client有一个名为Name.
Now I'm developing a re-usable application that will be used by many different customers. All customers agree that a client should have a name, hence it being in the base-class.
现在,我正在开发一个可供许多不同客户使用的可重复使用的应用程序。所有客户都同意客户应该有一个名字,因此它在基类中。
Now Customer A comes along and says he also need to track the client's Weight. Customer B doesn't need the Weight, but he wants to track Height. Customer C wants to track both Weight and Height.
现在客户 A 过来说他还需要跟踪客户的体重。客户 B 不需要体重,但他想跟踪身高。客户 C 想要跟踪体重和身高。
With traits, we could make the both the Weight and the Height features traits:
使用 trait,我们可以使 Weight 和 Height 特征都具有 trait:
class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight
Now I can meet all my customers' needs without adding any extra fluff to the class. If my customer comes back later and says "Oh, I really like that feature, can I have it too?", I just update the class definition to include the extra trait.
现在我可以满足我所有客户的需求,而无需在课堂上添加任何额外的绒毛。如果我的客户稍后回来并说“哦,我真的很喜欢那个功能,我也可以拥有它吗?”,我只需更新类定义以包含额外的特征。
How would you accomplish this in C#?
你将如何在 C# 中实现这一点?
Interfaces don't work here because I want concrete definitions for the properties and any associated methods, and I don't want to re-implement them for each version of the class.
接口在这里不起作用,因为我想要属性和任何相关方法的具体定义,而且我不想为类的每个版本重新实现它们。
(By "customer", I mean a literal person who has employed me as a developer, whereas by "client" I'm referring a programming class; each of my customers has clients that they want to record information about)
(“客户”是指雇用我担任开发人员的字面意思,而“客户”是指编程课程;我的每个客户都有他们想要记录有关信息的客户)
采纳答案by Lucero
You can get the syntax by using marker interfaces and extension methods.
您可以通过使用标记接口和扩展方法来获取语法。
Prerequisite: the interfaces need to define the contract which is later used by the extension method. Basically the interface defines the contract for being able to "implement" a trait; ideally the class where you add the interface should already have all members of the interface present so that noadditional implementation is required.
先决条件:接口需要定义扩展方法稍后使用的契约。基本上,接口定义了能够“实现”特征的契约;理想情况下,您添加接口的类应该已经存在接口的所有成员,以便不需要额外的实现。
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
}
Use like this:
像这样使用:
var ca = new ClientA();
ca.IsHeavierThan(10); // OK
ca.IsTallerThan(10); // compiler error
Edit:The question was raised how additional data could be stored. This can also be addressed by doing some extra coding:
编辑:提出了如何存储额外数据的问题。这也可以通过做一些额外的编码来解决:
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;
}
}
And then, the trait methods can add and retrieve data if the "trait interface" inherits from IDynamicObject:
然后,如果“trait 接口”继承自,则 trait 方法可以添加和检索数据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
}
Note: by implementing IDynamicMetaObjectProvideras well the object would even allow to expose the dynamic data through the DLR, making the access to the additional properties transparent when used with the dynamickeyword.
注意:通过实现IDynamicMetaObjectProvider该对象甚至允许通过 DLR 公开动态数据,从而在与dynamic关键字一起使用时对附加属性的访问变得透明。
回答by mpen
C# language(at least to version 5) does not have support for Traits.
C#语言(至少到第 5 版)不支持 Traits。
However, Scala has Traits and Scala runs on the JVM (and CLR). Therefore, it's not a matter of run-time, but simply that of the language.
但是,Scala 具有 Traits,并且 Scala 可以在 JVM(和 CLR)上运行。因此,这不是运行时间的问题,而只是语言的问题。
Consider that Traits, at least at the Scala sense, can be thought of as "pretty magic to compile in proxy methods" (they do notaffect the MRO, which is different from Mixins in Ruby). In C# the way to get this behavior would be to use interfaces and "lots of manual proxy methods" (e.g. composition).
考虑到 Traits,至少在 Scala 的意义上,可以被认为是“在代理方法中编译非常神奇”(它们不影响 MRO,这与 Ruby 中的 Mixins 不同)。在 C# 中,获得这种行为的方法是使用接口和“大量手动代理方法”(例如组合)。
This tedious process could be done with a hypothetical processor (perhaps automatic code generation for a partial class via templates?), but that's not C#.
这个乏味的过程可以用一个假设的处理器来完成(也许通过模板为部分类自动生成代码?),但这不是 C#。
Happy coding.
快乐编码。
回答by RJ Lohan
This sounds like PHP's version of Aspect Oriented Programming. There are tools to help like PostSharp or MS Unity in some cases. If you want to roll-your-own, code-injection using C# Attributes is one approach, or as suggested extension methods for limited cases.
这听起来像是 PHP 的面向方面编程的版本。在某些情况下,有一些工具可以提供帮助,例如 PostSharp 或 MS Unity。如果您想自己动手,使用 C# 属性的代码注入是一种方法,或者作为针对有限情况的建议扩展方法。
Really depends how complicated you want to get. If you are trying to build something complex I'd be looking at some of these tools to help.
真的取决于你想要变得多么复杂。如果您正在尝试构建一些复杂的东西,我会查看其中一些工具来提供帮助。
回答by weston
This is really an suggested extension to Lucero's answer where all the storage was in the base class.
这确实是 Lucero 答案的建议扩展,其中所有存储都在基类中。
How about using dependency properties for this?
为此使用依赖属性怎么样?
This would have the effect of making the client classes light weight at run time when you have many properties that are not always set by every descendant. This is because the values are stored in a static member.
当您有许多并非总是由每个后代设置的属性时,这将具有使客户端类在运行时轻量级的效果。这是因为值存储在静态成员中。
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;
}
}
回答by Pierre Arnaud
I'd like to point to NRoles, an experiment with rolesin C#, where rolesare similar to traits.
我想指出NRoles,这是一个在 C# 中使用角色的实验,其中角色类似于traits。
NRoles uses a post-compiler to rewrite the IL and inject the methods into a class. This allows you to write code like that:
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> { }
where class Radioimplements RSwitchableand RTunable. Behind the scenes, Does<R>is an interface with no members, so basically Radiocompiles to an empty class. The post-compilation IL rewriting injects the methods of RSwitchableand RTunableinto Radio, which can then be used as if it really derived from the two roles(from another assembly):
其中类Radio实现RSwitchable和RTunable。在幕后,Does<R>是一个没有成员的接口,所以基本上Radio编译成一个空类。后编译IL重写内喷射的方法RSwitchable和RTunable成Radio,因为如果真的来自两个来源的,其然后可用于角色(从另一个组件):
var radio = new Radio();
radio.TurnOn();
radio.Seek(42);
To use radiodirectly before rewriting happened (that is, in the same assembly as where the Radiotype is declared), you have to resort to extensions methods As<R>():
要radio在重写发生之前直接使用(即,在与Radio声明类型相同的程序集中),您必须求助于扩展方法As<R>():
radio.As<RSwitchable>().TurnOn();
radio.As<RTunable>().Seek(42);
since the compiler would not allow to call TurnOnor Seekdirectly on the Radioclass.
因为编译器不允许调用TurnOn或Seek直接在Radio类上。
回答by Pierre Arnaud
There is an academic project, developed by Stefan Reichart from the Software Composition Group at the University of Bern (Switzerland), which provides a true implementation of traitsto the C# language.
伯尔尼大学(瑞士)软件组合组的 Stefan Reichart 开发了一个学术项目,该项目为 C# 语言提供了真正的特征实现。
Have a look at the paper (PDF) on CSharpTfor the full description of what he has done, based on the mono compiler.
查看CSharpT上的论文 (PDF),以了解他基于单声道编译器所做工作的完整描述。
Here is a sample of what can be written:
以下是可以写入的示例:
trait TCircle
{
public int Radius { get; set; }
public int Surface { get { ... } }
}
trait TColor { ... }
class MyCircle
{
uses { TCircle; TColor }
}
回答by mpen
Building on what Lucero suggested, I came up with this:
基于Lucero 的建议,我想出了这个:
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;
}
}
Output:
输出:
Adam is 149.9 lbs.
Bob is 68.9 inches tall.
Cheryl is 119.9 lbs and 65.0 inches.
It isn't quite as nice as I'd like, but it's not too bad either.
它没有我想要的那么好,但也不算太糟糕。
回答by Panagiotis Kanavos
Traits can be implemented in C# 8 by using default interface methods. Java 8 introduced default interface methods for this reason too.
可以使用默认接口方法在 C# 8 中实现特征。由于这个原因,Java 8 也引入了默认接口方法。
Using C# 8, you can write almost exactly what you proposed in the question. The traits are implemented by the IClientWeight, IClientHeight interfaces that provide a default implementation for their methods. In this case, they just return 0:
使用 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;}
}
ClientAand ClientBhave the traits but don't implement them. ClientC implements only IClientHeightand returns a different number, in this case 16 :
ClientA并ClientB具有特征但不实施它们。ClientC 仅实现IClientHeight并返回不同的数字,在本例中为 16 :
class ClientA : Client, IClientWeight{}
class ClientB : Client, IClientHeight{}
class ClientC : Client, IClientWeight, IClientHeight
{
public int getHeight()=>16;
}
When getHeight()is called in ClientBthrough the interface, the default implementation is called. getHeight()can only be called through the interface.
当通过接口getHeight()调用时,调用ClientB默认实现。getHeight()只能通过接口调用。
ClientC implements the IClientHeight interface so its own method is called. The method is available through the class itself.
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 exampleshows the code produced from this example
这个 SharpLab.io 示例显示了从这个示例生成的代码
Many of the traits features described in the PHP overview on traitscan be implemented easily with default interface methods. Traits (interfaces) can be combined. It's also possible to define abstractmethods to force classes to implement certain requirements.
PHP 特性概述中描述的许多特性特性可以使用默认接口方法轻松实现。特征(接口)可以组合。也可以定义抽象方法来强制类实现某些要求。
Let's say we want our traits to have sayHeight()and sayWeight()methods that return a string with the height or weight. They'd need some way to force exhibiting classes (term stolen from the PHP guide) to implement a method that returns the height and weight :
比方说,我们希望我们的性状有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{}
The clients now haveto implement thet getHeight()or getWeight()method but don't need to know anything about the saymethods.
客户端现在必须实现 thetgetHeight()或getWeight()方法,但不需要了解有关这些say方法的任何信息。
This offers a cleaner way to decorate
这提供了一种更清洁的装饰方式
SharpLab.io linkfor this sample.
此示例的SharpLab.io 链接。

