C# 不可变 class 子 class

C# Immutable class sub class

我有一个不可变类型,我想创建它的一个子class,它可以访问所有相同的方法。

但是,由于您必须如何实现不可变 class,基本 class 方法 return 我的父类型,而不是我的子类型。是否有可能创建一个不可变 class 可以有子 class 是 return 子 class?

下面是 LinqPad 中 运行 的示例代码,演示了该问题

void Main()
{
    var immutable = new MyImmutable(new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, -5 },
        { ImmutableKey.Key3, 1.25m },
    });
    
    var immutable2 = new MyImmutable(new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, 2 },
        { ImmutableKey.Key3, 3 },
    });
    
    var added = immutable.Apply((a, b) => a + b, immutable2);
    added[ImmutableKey.Key1].Dump();
    added[ImmutableKey.Key2].Dump();
    added[ImmutableKey.Key3].Dump();
    
    var subImmutable1 = new SubImmutable(1, new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, -5 },
        { ImmutableKey.Key3, 1.25m },
    });
    var subImmutable2 = new SubImmutable(1, new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, 2 },
        { ImmutableKey.Key3, 3 },
    });
    
    var subImmutableAdded = subImmutable1.Apply((a, b) => a + b, subImmutable2);
    subImmutableAdded.GetType().Name.Dump(); //prints MyImmutable, it's not a SubImmutable
    //after adding two SubImmutables, the type is changed back to the base type
    
    var asSub = (SubImmutable)subImmutableAdded; // Unable to cast object of type 'MyImmutable' to type 'SubImmutable', SomeOtherValue was lost.
}

public enum ImmutableKey 
{
    Key1,
    Key2,
    Key3
}

public class MyImmutable
{
    protected static readonly IEnumerable<ImmutableKey> AllKeys = Enum.GetValues(typeof(ImmutableKey)).Cast<ImmutableKey>();
    
    private Dictionary<ImmutableKey, decimal> _dict { get; set; }
    
    public MyImmutable(Dictionary<ImmutableKey,decimal> d)
    {
        _dict = d;
    }
    
    public decimal this[ImmutableKey key]
    {
        get
        {
        if (_dict == null || !_dict.ContainsKey(key))
            return 0;

        return _dict[key];
        }
    }
    
    public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)
    {
        var aggregated = new Dictionary<ImmutableKey, decimal>(AllKeys.Count());
        foreach (ImmutableKey bt in AllKeys)
        {
            aggregated[bt] = aggFunc(this[bt], y[bt]);
        }
        return new MyImmutable(aggregated);
    }
}

public class SubImmutable : MyImmutable
{
    public int SomeOtherValue { get; set; }
    public SubImmutable(int someValue, Dictionary<ImmutableKey,decimal> d)
        :base(d)
    {
        SomeOtherValue= someValue;
    }
}

输出:

2

-3

4.25

MyImmutable

InvalidCastException: Unable to cast object of type 'MyImmutable' to type 'SubImmutable'.

有没有一种方法可以让我拥有一个继承的不可变类型,它可以继承基类型中的所有方法,而不必全部重新实现它们?

配套代码审查问题:https://codereview.stackexchange.com/questions/79380/inheriting-methods-of-an-immutable-type

您可以使用虚拟方法获取新实例。

在基础 class 中创建一个虚方法,该方法接受输入以创建基础的新实例 class 和 return 基础的新实例 class.然后在 subclass 中覆盖它以生成 subclass 需要的任何额外输入和 return subclass.

的新实例
public class MyImmutable
{
    // other stuff

    // add this method
    protected virtual MyImmutable GetNew(Dictionary<ImmutableKey, decimal> d)
    {
        return new MyImmutable(d);
    }

    // modify this method as shown
    public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)
    {
        var aggregated = new Dictionary<ImmutableKey, decimal>(AllKeys.Count());
        foreach (ImmutableKey bt in AllKeys)
        {
            aggregated[bt] = aggFunc(this[bt], y[bt]);
        }
        return GetNew(aggregated);
    }
}

public class SubImmutable : MyImmutable
{
    // other stuff

    // add this method
    protected override MyImmutable GetNew(Dictionary<ImmutableKey, decimal> d)
    {
        return new SubImmutable(SomeOtherValue, d);
    }
}

这样,任何不关心子class额外内容的转换都不需要在子class中被覆盖。

某些转换可能仍需要覆盖。例如:

var one = new SubImmutable(1, alpha);
var two = new SubImmutable(2, alpha);
var test1 = one.Apply((a, b) => a + b, two);
var test2 = two.Apply((a, b) => a + b, one);
Console.WriteLine(test1[someKey] == test2[someKey]); // true
Console.WriteLine(test1.SomeOtherValue == test2.SomeOtherValue); // false

如果您希望 test1test2 具有相同的 SomeOtherValue,那么您必须将 Apply 方法设为虚拟方法,然后在子class.

结合不可变性和继承的主要问题之一是您希望像 Apply 这样的操作接受并调用派生 class 的 return 实例,而不是基础 class

那是你希望 MyImmutable.Apply 成为:
public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)

SubImmutable.Apply为:
public SubImmutable Apply(Func<decimal, decimal, decimal> aggFunc, SubImmutable y)

您可以通过创建一个抽象基础 class 巧妙地解决这个问题,所有具体的 classes(MyImmutable 和 SubImmutable)都从使用 'curiously recurring template pattern'

派生而来

见下文,我还根据自己的喜好稍微更改了您的代码 :) 请注意,这里的 Dict 不是只读的,因此 classes 是公开的(因此有效地)不可变但在内部是可变的。

public enum ImmutableKey { Key1, Key2, Key3 }

abstract class MyImmutableBase<TDerived> where TDerived : MyImmutableBase<TDerived> {
  protected static readonly IEnumerable<ImmutableKey> AllKeys = Enum.GetValues(typeof(ImmutableKey)).Cast<ImmutableKey>();
  private ImmutableDictionary<ImmutableKey, decimal> Dict;

  public MyImmutableBase() => Dict = ImmutableDictionary<ImmutableKey, decimal>.Empty;

  protected abstract TDerived GetNew();

  public decimal this[ImmutableKey key] { get { if (Dict == null || !Dict.ContainsKey(key)) return 0; return Dict[key]; } }

  public TDerived Add(IEnumerable<KeyValuePair<ImmutableKey, decimal>> d) {
    var res = GetNew();
    res.Dict = res.Dict.AddRange(d);
    return res;
  }

  public TDerived Apply(Func<decimal, decimal, decimal> aggFunc, TDerived y) {
    var aggregated = ImmutableDictionary<ImmutableKey, decimal>.Empty;
    foreach (ImmutableKey bt in AllKeys) aggregated = aggregated.SetItem(bt, aggFunc(this[bt], y[bt]));
    return GetNew().Add(aggregated);
  }
}


class MyImmutable : MyImmutableBase<MyImmutable> {
  protected override MyImmutable GetNew() => new();
}

class SubImmutable : MyImmutableBase<SubImmutable> {
  public int SomeOtherValue { get; init; }
  public SubImmutable(int someValue) : base() => SomeOtherValue = someValue;
  protected override SubImmutable GetNew() => new(SomeOtherValue);
}