5

I have a type that is an immutable type, and I'd like to create a subclass of it that has access to all the same methods.

However, because of how you have to implement an immutable class, the base class methods return my parent type, not my child type. Is it possible to create an immutable class that can have sub classes that return the subclass?

Below is example code to run in LinqPad that demonstrates the problem

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

Output:

2

-3

4.25

MyImmutable

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

Is there a way I can have an inherited immutable type that can inherit all the methods in the base type without having to reimplement them all?

Companion CodeReview question: https://codereview.stackexchange.com/questions/79380/inheriting-methods-of-an-immutable-type

asked Feb 4, 2015 at 16:49
4
  • You should take a look at ImmutableCollections from Microsoft. Commented Feb 4, 2015 at 16:52
  • You could use method hiding with new. But in my experience inheritance and immutability don't mix well. You'd rather use readonly interfaces together with sealed concrete classes. Commented Feb 4, 2015 at 16:52
  • @CodesInChaos The whole advantage of using immutable types is that you can actually rely on the type never changing. If there are methods that allow the type to be mutated, even if they're hidden, those assumptions can end up being violated. Commented Feb 4, 2015 at 16:53
  • @Greg I'm using an ImmutableDictionary in my actual implementation, this was an example. Commented Feb 4, 2015 at 17:02

2 Answers 2

2

You can use a virtual method to get the new instance.

Make a virtual method in the base class that takes the input to create a new instance of the base class and returns a new instance of the base class. Then override it in the subclass to generate any additional inputs the subclass needs and return a new instance of the 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);
 }
}

This way any transformations that don't care about the subclass' extra stuff don't need to be overridden in the subclass.

Some transformations may still need to be overridden. For example:

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

If you want test1 and test2 to have the same SomeOtherValue, then you'll have to make the Apply method virtual and then override it in the subclass.

answered Feb 4, 2015 at 17:00

3 Comments

so i'd have to override every method?
@DLeh No, have all your transforms use the virtual GetNew. Then you just need to override GetNew in the subclass. Note that this assumes the subclass doesn't need extra information to complete the transformation. Your example just wants to carry extra information, and this approach should work fine for that.
oh I see now. Cool! I'll try this out and get back to you.
1

One of the main issues with combining immutability and inheritance is that you want operations like your Apply to accept and return instances of the derived class it is called on, not the base class

That is you want MyImmutable.Apply to be:
public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)

and SubImmutable.Apply to be:
public SubImmutable Apply(Func<decimal, decimal, decimal> aggFunc, SubImmutable y)

You can neatly solve this by creating an abstract base class that all concrete classes (MyImmutable and SubImmutable) derive from using the 'curiously recurring template pattern'

See below, I also changed your code a bit to my taste :) note that Dict here is not readonly so the classes are publicly (and so effectively) immutable but internally mutable.

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);
}
answered Oct 10, 2021 at 21:02

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.