我知道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
现在,我可以满足所有客户的需求,而不会给课程增加任何额外的负担。如果我的客户稍后回来并说“哦,我真的很喜欢这个功能,我也可以拥有它吗?”,我只是更新了类定义以包含额外的特性。
你将如何在C#中实现这一目标?
接口在这里不起作用,因为我想要对属性和任何相关方法的具体定义,我不想为每个版本的类重新实现它们。
(“客户”,我指的是雇用我作为开发人员的文字人员,而“客户”我指的是编程课程;我的每个客户都有他们想要记录信息的客户<) / p>
答案 0 :(得分:50)
您可以使用标记接口和扩展方法来获取语法。
先决条件:接口需要定义稍后由扩展方法使用的合同。基本上,界面定义了能够“实现”特征的合同;理想情况下,添加接口的类应该已经存在接口的所有成员,以便 no 需要额外的实现。
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
关键字一起使用时,可以透明地访问其他属性。
答案 1 :(得分:8)
C#语言(至少是版本5)不支持Traits。
但是,Scala在JVM(和CLR)上运行Traits和Scala。因此,这不是运行时的问题,而只是语言的问题。
考虑Traits,至少在Scala意义上,可以被认为是“在代理方法中编译的神奇”(它们不影响MRO,这与Ruby中的Mixins不同) )。在C#中,获得此行为的方法是使用接口和“大量手动代理方法”(例如组合)。
这个繁琐的过程可以用一个假设的处理器完成(也许是通过模板为部分类自动生成代码?),但这不是C#。
快乐的编码。
答案 2 :(得分:5)
我想指出NRoles,在C#中使用角色进行的实验,其中角色与 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
,然后可以将其用作真正来自两个角色的方法(来自另一个集会):
var radio = new Radio();
radio.TurnOn();
radio.Seek(42);
要在重写之前直接使用radio
(即,在声明Radio
类型的同一程序集中),您必须求助于扩展方法As<R>
():
radio.As<RSwitchable>().TurnOn();
radio.As<RTunable>().Seek(42);
因为编译器不允许直接在TurnOn
类上调用Seek
或Radio
。
答案 3 :(得分: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 }
}
答案 4 :(得分:3)
这实际上是对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;
}
}
答案 5 :(得分:2)
在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.
它并不像我想的那么好,但它也不是太糟糕。
答案 6 :(得分: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;
}
通过接口在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 interface
var clientC=new ClientC();
clientC.getHeight();
}
}
This SharpLab.io example显示了此示例产生的代码
PHP overview on traits中描述的许多特征功能都可以使用默认接口方法轻松实现。特性(接口)可以组合。也可以定义抽象方法来强制类实现某些要求。
假设我们希望特征具有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(该示例)。
答案 7 :(得分:0)
这听起来像PHP的面向方面编程版本。在某些情况下,有一些工具可以帮助像PostSharp或MS Unity。如果您想自己滚动,使用C#属性进行代码注入是一种方法,或者作为有限情况的建议扩展方法。
真的取决于你想要的复杂程度。如果你正在尝试构建复杂的东西,我会看一些这些工具来帮助你。