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

11
说实话,我不确定该如何表达这个问题,如果实际问题与标题不符,请原谅。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);

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


1
如果接口适合您的需求,那么interface IDataMold<T>将作为List<IDataMold<T>> - Austin T French
通常情况下,这看起来像是 class TextMold : DataMold<string> { public string Result { get; set; } } 或者只有一个 getter: class TextMold : DataMold<string> { public string Result { get; private set; } } - Mark Schultheiss
4个回答

11
简短回答:你不能。
关于泛型的一个反直觉之处是它们没有关系。例如,List<int>List<string>没有任何关系。它们不相互继承,也不能将一个转换为另一个。
你可以声明协变关系,它看起来很像继承关系,但不能在你声明的intstring之间,因为一个是值类型,一个是引用类型。
你唯一的选择是添加另一个它们共同拥有的接口,像这样:
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());
}

4
你正在寻找协变性。请看 T 泛型类型参数前的 out 关键字:covariance
// 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>());

2
您可以使用访问者模式:
添加一个访问者接口,接受所有类型,并实现一个访问者,执行您想要应用于所有DataMolds的操作。
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

1

动态

可以使用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 的含义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接