Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a subclass constructor based on a parent class instance?

I have an Item and a subclass AdvancedItem (all made of value-types if that matters):

public Item
{
    public string A;
    public bool B;
    public char C;
    ...// 20 fields
}

public AdvancedItem : Item
{
    public string Z;
}

It's easy to simply create an Item or an AdvancedItem independently:

var item = new Item { A = "aa", B = true, C = 'c', ... };
var aItem = new AdvancedItem { A = "aa", B = true, C = 'c', ..., Z = "zz" };

Now, I just want to turn an Item into an AdvancedItem by providing it the string Z separately. In order to achieve that I was thinking of using a constructor.

Attempt A:

// annoying, we are not using the inheritance of AdvancedItem:Item
// so we will need to edit this whenever we change the class Item
public AdvancedItem(Item item, string z)
{
    A = item.A;
    B = item.B;
    ...;//many lines
    Z = z;
}

Attempt B:

// to use inheritance it seems I need another constructor to duplicate itself
public Item(Item item)
{
    A = item.A;
    B = item.B;
    ...;//many lines
}

public AdvancedItem(Item item, string z) : base(Item)
{
    Z = z;
}

Is there any way to improve this second attempt to avoid writing many lines of X = item.X? Maybe a solution to auto-clone or auto-duplicate a class with itself where public Item(Item item) would be wrote in one line?

like image 397
Cœur Avatar asked Jun 20 '14 11:06

Cœur


2 Answers

Consider using AutoMapper to copy properties between objects.

This would allow the following:

Item a = new Item { A = 3, B = 'a', .... };

AdvancedItem advanced= Mapper.Map<AdvancedItem>(a);
string z = "Hello World";
advanced.Z = z;

Update

If you do not want to use AutoMapper you can use Reflection or better, Expressions. However this will make your code a bit more complex

Consider these two types:

class Item
{
    public int A, B, C;
    public string D, E, F;
    private int privateInt;

    public Item(int valueOfPrivateField)
    {
        privateInt = valueOfPrivateField;
    }
}

class AdvancedItem : Item
{
    public string G;

    public AdvancedItem(int valueOfPrivateField) : base(valueOfPrivateField)
    {
    }
}

We can define a method that creates a field-wise copy expression. Since you mention that all your fields are value types we can just copy each field one by one to the other object:

    private static void MapFields<T>(T target, T source)
    {
        Type type = typeof (T);
        if (!Mappers.ContainsKey(type))
        {
            //build expression to copy fields from source to target;
            var targetParam = Expression.Parameter(typeof(object));
            var targetCasted = Expression.TypeAs(targetParam, typeof(T));
            var sourceParam = Expression.Parameter(typeof(object));
            var sourceCasted = Expression.TypeAs(sourceParam, typeof(T));
            var setters = new List<Expression>();
            //get all non-readonly fields
            foreach (var fieldInfo in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(f => !f.IsInitOnly))
            {
                Expression targetField = Expression.Field(targetCasted, fieldInfo);
                Expression sourceField = Expression.Field(sourceCasted, fieldInfo);
                setters.Add(Expression.Assign(targetField, sourceField));
            }
            Expression block = Expression.Block(setters);

            var mapperFunction = Expression.Lambda<Action<object, object>>(block, targetParam,
                sourceParam).Compile();
            Mappers[type] = mapperFunction;
        }
        Mappers[type](target, source);

    }

    private static readonly Dictionary<Type, Action<object, object>> Mappers =
        new Dictionary<Type, Action<object, object>>();

This caches functions that map all fields from the source to the target object, and should have close to the same performance as manually writing this.A = A, this.B = B etc.

Calling the method:

    static void Main(string[] args)
    {
        var item = new Item(56) {A = 5, B = 6};

        var advanced = new AdvancedItem(0);

        MapFields(advanced, item);
        int a = advanced.A; //5
        int b = advanced.B; //6;
        //note that advanced.privateInt == 56!
    }

Please note that this code is more complex and less reliable than AutoMapper and is not recommended or ready for production systems.

like image 68
Bas Avatar answered Oct 23 '22 09:10

Bas


One object-oriented way to implement this kind of thing for class hierarchies is to introduce a protected copy constructor in the base class (although it still requires you to write all the assignments):

public class Item
{
    protected Item(Item other)
    {
        this.A = other.A;
        this.B = other.B;
        this.C = other.C;
    }

    public string A;
    public bool B;
    public char C;
    // 20 fields
}

Then you would call that from the derived class like so:

public class AdvancedItem : Item
{
    public AdvancedItem(Item item, string z): base(item)
    {
        Z = z;
    }

    public string Z;
}

Note that this approach does NOT prevent you from having to write all the assignment lines, but you only need to write them once, and it does mean that you now have a protected copy constructor available, which can be very useful. Also, the assignments are now all in the base class where they belong.

This approach is extendable to further derived classes. You can introduce a protected copy constructor to AdvancedItem written in terms of the public constructor (to avoid duplicated code).

For example:

public class AdvancedItem : Item
{
    protected AdvancedItem(AdvancedItem other): this(other, other.Z)
    {
    }

    public AdvancedItem(Item item, string z): base(item)
    {
        Z = z;
    }

    public string Z;
}

public class EvenMoreAdvancedItem: AdvancedItem
{
    public EvenMoreAdvancedItem(AdvancedItem advancedItem, double q): base(advancedItem)
    {
        Q = q;
    }

    public double Q;
}
like image 4
Matthew Watson Avatar answered Oct 23 '22 10:10

Matthew Watson