59

我知道 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# 中完成此任务?

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

(“客户”是指雇佣我作为开发人员的字面意义上的人,而“客户”指的是编程课程;我的每个客户都有他们想要记录信息的客户)

4

8 回答 8

58

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

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

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关键字一起使用时使对附加属性的访问变得透明。

于 2012-05-23T23:36:44.897 回答
12

Traits 可以通过使用默认接口方法在 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;}
}

ClientAClientB具有特征但不实现它们。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 class
        var clientC = new ClientC();        
        clientC.getHeight();
    }
}

此 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{}

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

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

此示例的SharpLab.io 链接

于 2018-11-23T16:44:18.540 回答
10

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

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

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

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

快乐编码。

于 2012-05-23T23:30:36.953 回答
10

我想指出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实现RSwitchableRTunable。在幕后,Does<R>是一个没有成员的接口,所以基本上Radio编译成一个空类。编译后 IL 重写将RSwitchableRTunableinto 的方法注入Radio,然后可以像真的从两个角色(来自另一个程序集)派生一样使用它们:

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

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

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

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

于 2014-09-02T09:09:22.650 回答
8

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

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

以下是可以编写的示例:

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

trait TColor { ... }

class MyCircle
{
    uses { TCircle; TColor }
}
于 2014-09-02T09:14:23.183 回答
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;
    }
}
于 2012-05-24T10:57:19.280 回答
3

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

输出:

Adam is 149.9 lbs.
Bob is 68.9 inches tall.
Cheryl is 119.9 lbs and 65.0 inches.

它没有我想要的那么好,但也不算太糟糕。

于 2015-03-30T20:05:48.107 回答
0

这听起来像是 PHP 的面向方面编程的版本。在某些情况下,有一些工具可以提供帮助,例如 PostSharp 或 MS Unity。如果你想自己动手,使用 C# 属性的代码注入是一种方法,或者作为有限情况的建议扩展方法。

真的取决于你想变得多复杂。如果您正在尝试构建复杂的东西,我会查看其中一些工具来提供帮助。

于 2012-05-23T23:59:04.437 回答