泛型类多态性

3
如果我有以下内容:
public abstract class Parameter<T>  
{
    protected T value;

    public virtual T Value
    {
        get { return value; }
        set { this.value = value; }
    }

    protected Parameter(T startingValue)
    {
        value = startingValue;
    }
}

public class FloatParameter : Parameter<float> 
{
    public FloatParameter(float startingValue) : base(startingValue){}
}

public class IntParameter : Parameter<int> 
{
    public override int Value
    {
        get { return value; }
        set { this.value = value > 100 ? 100 : value; }
    }

    public IntParameter(int startingValue) : base (startingValue) {}
}

有没有办法创建一些可以包含任何派生类型的 List<Parameter>?例如,类似于以下内容:

// no type specified in Parameter
List<Parameter> storedParameters = new List<Parameter>(); 
storedParameters.Add(new FloatParameter(2f));
storedParameters.Add(new IntParameter(7));

foreach(Parameter p in storedParameters)
{
    DoSomethingWithValue(p.Value);
}

或者,如果这种实现有缺陷,有更好的方法吗?我目前的做法感觉有些幼稚。


为什么你想把芒果放在橙子列表中? - Ehsan Sajjad
这个实现的问题在于你尝试使用的两种类型。Int和float都是结构体,这意味着一旦你将T分配给其中一个,就不能添加另一个。 - Weggo
你能添加 DoSomethingWithValue 的代码吗? - Hamid Pourjam
1
DoSomethingWithValue是一个通用方法,它在运行时对值进行类型检查。而且Value应该与泛型指定的类型相同。所以FloatParameterValue属性应该返回一个浮点数。 - Danny Herbert
如果DoSomethingWithValue进行运行时类型检查,则应在层次结构中的每个类中以多态方式实现。 - Hamid Pourjam
显示剩余3条评论
5个回答

3

我认为管理这种情况的唯一方法是拥有一个接口来管理通用类型,类似下面的东西应该可以工作:

public interface IParameter
{
    void DoSomething();
}

public abstract class Parameter<T>
{
    protected T value;

    public T Value
    {
        get { return value; }
        set { this.value = value; }
    }

    protected Parameter(T startingValue)
    {
        value = startingValue;
    }
}

public class FloatParameter : Parameter<float>, IParameter
{
    public FloatParameter(float startingValue) : base(startingValue) { }
    public void DoSomething()
    {
        Console.WriteLine(value);
    }
}

public class IntParameter : Parameter<int>, IParameter
{
    public IntParameter(int startingValue) : base(startingValue) { }

    public void DoSomething()
    {
        Console.WriteLine(value);
    }
}

在这种情况下,您将能够创建一个接口IParameter的列表,并在其中添加特定的实例:
 var list = new List<IParameter>();
 list.Add(new FloatParameter(1F));
 list.Add(new IntParameter(1));

 foreach (var item in list)
 {
      item.DoSomething();
 }

这是一种非常好的方法。但是,你能想到一种方式让我能够从“item” foreach变量中访问“Parameter”对象的属性吗? - Danny Herbert
@DannyHerbert 这是不可能的。编译器无法知道列表中存储的具体类型(实际上,这可能取决于用户输入,在程序运行之前是不可用的)。因此,它无法提供对更具体类型上属性的访问,因为它无法知道它们是否实际存在。 - Kyle

2
尝试添加非泛型接口。以下是一个示例:
public class Program
{
    static void Main(string[] args)
    {          
        try
        {
            List<IParameter> storedParameters = new List<IParameter>();
            storedParameters.Add(new FloatParameter(2f));
            storedParameters.Add(new IntParameter(7));

            foreach (IParameter p in storedParameters)
            {
                Console.WriteLine(p.ToString());
            }

        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

public interface IParameter
{
    object value { get; }
}

public class Parameter<T> : IParameter
{
    public object value { get; protected set; }

    public virtual T Value
    {
        get { return (T)value; }
        set { this.value = value; }
    }


    protected Parameter(T startingValue)
    {
        value = startingValue;
    }
}

public class FloatParameter : Parameter<float>
{
    public FloatParameter(float startingValue) : base(startingValue){ }
}

public class IntParameter : Parameter<int>
{
    public override int Value
    {
        get { return (int)value; }
        set { this.value = value > 100 ? 100 : value; }
    }

    public IntParameter(int startingValue) : base (startingValue) { }
}

2
将原始类型 T 存储在 object 字段中需要进行装箱 / 拆箱操作,每次通过 T Value 属性访问时都需要这样做。另外,如果显式实现了 IParameter 接口,则可以使用相同的属性名称。 - Pieter Witvoet

1
如果您将值更改为对象,则可以将该值设置为任何类型:
class Program
{
    static void Main(string[] args)
    {

        // no type specified in Parameter
        var storedParameters = new List<ParameterBase>();
        storedParameters.Add(new FloatParameter(3.5F));
        storedParameters.Add(new IntParameter(7));

        foreach (var p in storedParameters)
        {
            Console.WriteLine(p.Value);
        }
    }
}

public class ParameterBase
{
    protected object value;

    public virtual object Value
    {
        get { return value; }
        set { this.value = value; }
    }
}

public class FloatParameter : ParameterBase
{
    public FloatParameter(float value)
    {
        Value = value;
    }
}

public class IntParameter : ParameterBase
{
    public IntParameter(int value)
    {
        Value = value;
    }
}

更新:根据@Pieter Witvoet的建议,使用对象代替动态,并移除ValueType。


3
为什么不使用objectdynamic 主要用于动态语言交互,并带有额外的成本(延迟绑定)。 - Pieter Witvoet
谢谢@Pieter Witvoet,我已经更新了代码以使用对象 :) - Weggo
还有一个问题:为什么类型必须要单独设置?这很容易被滥用:Value = "hello"; ValueType = typeof(int);。但是为什么还要将它存储起来呢?IntParameter 应该总是返回 typeof(int),所以为什么不将基类属性声明为抽象的,并在每个派生的 Parameter 类中进行重写呢?请注意 IntParameterFloatParameter 之间几乎没有什么差别 - 你可以使用单个泛型 Parameter<T> 类代替它们(当然,需要让 ValueType 返回 typeof(T))。 - Pieter Witvoet
它仅用于说明所设置的类型为int或float类型。 - Weggo

1
不,这是不可能的。
你想要做的是拥有一个界面(或基类),公开一个未定义类型的属性,以便能够检索该值并动态地将其分派到 DoSomethingWithValue 的适当重写。
通过将属性定义为 dynamic,而不是使用泛型,可以实现你想要的效果。
public class Parameter
{
    protected dynamic value;

    public dynamic Value
    {
        get { return value; }
        set { this.value = value; }
    }

    public Parameter(dynamic startingValue)
    {
        value = startingValue;
    }
}

public class MyStuff {
    public void DoStuff()
    {
        List<Parameter> storedParameters = new List<Parameter>();
        storedParameters.Add(new Parameter(2f));
        storedParameters.Add(new Parameter(7));

        foreach (Parameter p in storedParameters)
        {
            DoSomethingWithValue(p.Value);
        }
    }
}

否则,您应该考虑实现“双重分派(Double dispatch)”技术。

1
你可以通过定义一个共同的接口并使用访问者模式来实现。
public interface IParameterVisitor
{
    void VisitInt(int value);
    void VisitFloat(float value);
}

public interface IParameter
{
    void Accept(IParameterVisitor visitor);
}

之前的实现需要进行一些修改:

public abstract class Parameter<T> : IParameter
{
    protected T value;

    public virtual T Value
    {
        get { return value; }
        set { this.value = value; }
    }

    protected Parameter(T startingValue)
    {
        value = startingValue;
    }

    public abstract void Accept(IParameterVisitor visitor);
}

FloatParameter将会调用VisitFloat,而IntParameter将会调用VisitInt

public class FloatParameter : Parameter<float>
{
    public FloatParameter(float startingValue) : base(startingValue) { }
    public override void Accept(IParameterVisitor visitor)
    {
        visitor.VisitFloat(this.value);
    }
}

public class IntParameter : Parameter<int>
{
    public override int Value
    {
        get { return value; }
        set { this.value = value > 100 ? 100 : value; }
    }

    public override void Accept(IParameterVisitor visitor)
    {
        visitor.VisitInt(this.value);
    }

    public IntParameter(int startingValue) : base(startingValue) { }
}

我们的访客,例如:

public class MyVisitor : IParameterVisitor
{
    public void VisitInt(int value)
    {
        Console.WriteLine($"Visiting an int: {value}");
    }

    public void VisitFloat(float value)
    {
        Console.WriteLine($"Visiting a float: {value}");
    }
}

最终,用法如下:
var parameters = 
    new List<IParameter> {new FloatParameter(0.5f), new IntParameter(1)};
var visitor = new MyVisitor();
foreach (IParameter parameter in parameters) {
   parameter.Accept(visitor);
}

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