如何在 C# 中正确使用相关但不同的 类 上的调用方法

How do I properly work with calling methods on related but different classes in C#

老实说,我不确定如何表达这个问题,所以如果实际问题不是您根据标题所期望的,请原谅我。 C# 是我编写过的第一种静态类型语言,到目前为止,它的这一方面一直让我非常头疼。我很确定我对围绕如何以静态类型方式设计系统的核心思想没有很好的把握。

这是我正在尝试做的事情的粗略想法。假设我有一个 类 的层次结构,如下所示:

abstract class DataMold<T>
{
    public abstract T Result { get; }
}

class TextMold : DataMold<string>
{
  public string Result => "ABC";
}  

class NumberMold : DataMold<int>
{
   public int Result => 123
}

现在假设我想制作一个项目列表,其中项目可以是任何类型的模具,我可以在 foreach 循环中获得每个项目的 Result 属性像这样:

List<DataMold<T>> molds = new List<DataMold<T>>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (DataMold<T> mold in molds)
    Console.WriteLine(mold.Result);

您可能已经知道,这是行不通的。从我在搜索中读到的内容来看,这与我无法将列表声明为 DataMold<T> 类型有关。处理此类问题的正确方法是什么?

简短的回答:你不能。

关于泛型类型的一个违反直觉的事情是它们不相关。例如,List<int>List<string> 没有任何关系。它们不相互继承,您不能将一个转换为另一个。

可以声明一个covariance关系,这看起来很像继承关系,但不是在int和[=18=之间] 正如你声明的那样,因为一个是值类型,一个是引用类型。

您唯一的选择是添加另一个它们共有的接口,如下所示:

interface IDataMold
{
}

abstract class DataMold<T> : IDataMold
{
    public abstract T Result { get; }
}

现在您可以将所有模具存放在 List<IDataMold> 中。但是,该接口没有属性,因此您很难从中获取任何信息。您可以添加一些属性,但它们不会是特定于类型的,因为 IDataMold 没有通用类型参数。但是你可以添加一个常见的 属性

interface IDataMold
{
    string ResultString { get; }
}

...并实施它:

abstract class DataMold<T>
{
    public abstract T Result { get; }
    public string ResultString => Result.ToString();
}

但是如果您只需要为每个项目显示一个等效的字符串,您可以只重写 ToString() 而不是:

class TextMold : DataMold<string>
{
    public string Result => "ABC";
    public override string ToString() => Result.ToString();
}

现在你可以这样做了:

List<IDataMold> molds = new List<IDataMold>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (var mold in molds)
{
    Console.WriteLine(mold.ToString());
}

您正在寻找 covariance。参见T泛型类型参数前的out关键字:

// Covariance and contravariance are only possible for
// interface and delegate generic params
public interface IDataMold<out T> 
{
   T Result { get; }
}

abstract class DataMold<T> : IDataMold<T>
{
  public abstract T Result { get; }
}

class StringMold : DataMold<string> {}

class Whatever {}

class WhateverMold : DataMold<Whatever> {}

现在继承DataMold<T>并创建一个List<IDataMold<object>>:

var molds = new List<IDataMold<object>>();
molds.Add(new StringMold());
molds.Add(new WhateverMold());

顺便说一句,在将 IDataMold<int> 转换为 IDataMold<object> 时,您不能使用 协方差 。不要重复已经解释过的内容,请参阅其他问答:Why covariance and contravariance do not support value type

如果您真的被迫实施 IDataMold<int>,该列表可能是 object:

类型
var molds = new List<object>();
molds.add(new TextMold());
molds.add(new NumberMold());

您可以使用 Enumerable.OfType<T> 获取 molds:

的子集
var numberMolds = molds.OfType<IDataMold<int>>();
var textMolds = molds.OfType<IDataMold<string>>();

此外,您可以创建两个列表:

var numberMolds = new List<IDataMold<int>>();
var textMolds = new List<IDataMold<string>>();

因此,如果需要,您可以稍后将它们混合为 IEnumerable<object>

var allMolds = numberMolds.Cast<object>().Union(textMolds.Cast<object>());

您可以使用访问者模式:

添加一个接受所有类型的访问者界面,并实现一个访问者来执行您想要应用于所有 DataMold 的操作:

interface IDataMoldVisitor
{  void visit(DataMold<string> dataMold);
   void visit(DataMold<int> dataMold);
}

// Console.WriteLine for all
class DataMoldConsoleWriter : IDataMoldVisitor
{  public void visit(DataMold<string> dataMold)
   {  Console.WriteLine(dataMold.Result);
   }
   public void visit(DataMold<int> dataMold)
   {  Console.WriteLine(dataMold.Result);
   }
}

添加一个您的列表可以容纳的接受器接口,并让您的 DataMold 类 实现它:

interface IDataMoldAcceptor
{  void accept(IDataMoldVisitor visitor);
}

abstract class DataMold<T> : IDataMoldAcceptor
{  public abstract T Result { get; }
   public abstract void accept(IDataMoldVisitor visitor);
}

class TextMold : DataMold<string>
{  public string Result => "ABC";
   public override void accept(IDataMoldVisitor visitor)
   {  visitor.visit(this);
   }
}  

class NumberMold : DataMold<int>
{  public int Result => 123;
   public override void accept(IDataMoldVisitor visitor)
   {  visitor.visit(this);
   }
}

最后,执行它:

// List now holds acceptors
List<IDataMoldAcceptor> molds = new List<IDataMoldAcceptor>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

// Construct the action you want to perform
DataMoldConsoleWriter consoleWriter = new DataMoldConsoleWriter();
// ..and execute for each mold
foreach (IDataMoldAcceptor mold in molds)
    mold.accept(consoleWriter);

输出为:

ABC
123

dynamic

这可以通过 dynamic 关键字来完成,但会牺牲性能和类型安全性。

var molds = new List<object>();     // System.Object is the most derived common base type.
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (dynamic mold in molds)
    Console.WriteLine(mold.Result);

现在 molddynamic,C# 将在 运行 时检查 mold 的类型,然后找出 .Result 表示从那里开始。