-->

Covariance in generic interfaces

2019-02-17 18:17发布

问题:

I wanted to create an observableCollection that is sortable so i started creating a class that inherit observable with some methods to sort it, then i wanted that class to persist the index into the childs, so i created an interface that expose an index property where i can write to, and i costrainted the T of my collection class to be of my Interface, then i wanted to be able from avery item to access the parentCollection and here the problems started because the type of the parent collection is generic ... i've tried many solutions, and i think covariance or invariance is the way, but i can't get it working ...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    public class SortableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>, ISortableCollection<T> where T : ISortable<T>
    {
        public void Sort()
        {
            //We all know how to sort something
            throw new NotImplementedException();
        }

        protected override void InsertItem(int index, T item)
        {
            item.Index = index;
            item.ParentCollection = this;
            base.InsertItem(index, item);
        }
    }

    public interface ISortableCollection<T> : IList<T>
    {
        void Sort();
    }

    public interface ISortable<T>
    {
        Int32 Index { get; set; }
        ISortableCollection<T> ParentCollection { get; set; }
    }

    public class BaseClass : ISortable<BaseClass>
    {
        public int Index { get; set; }

        public ISortableCollection<BaseClass> ParentCollection { get; set; }
    }

    public class DerivedClass : BaseClass { }

    public class Controller
    {
        SortableCollection<BaseClass> MyBaseSortableList = new SortableCollection<BaseClass>();
        SortableCollection<DerivedClass> MyDerivedSortableList = new SortableCollection<DerivedClass>();

        public Controller()
        {
            //do things
        }
    }
}

this is more or less the setup. I'd like to be able to create a SortableCollection<DerivedClass> but the types mismatch... wich is the correct way of doing it ?

exact error is

Error 1 The type 'ClassLibrary1.DerivedClass' cannot be used as type parameter 'T' in the generic type or method 'ClassLibrary1.SortableCollection<T>'. There is no implicit reference conversion from 'ClassLibrary1.DerivedClass' to 'ClassLibrary1.ISortable<ClassLibrary1.DerivedClass>'. c:\users\luigi.trabacchin\documents\visual studio 2013\Projects\ClassLibrary1\ClassLibrary1\Class1.cs 48 89 ClassLibrary1

回答1:

The problem is that your constraint on T is "T is required to be an I<T>", and you have passed a DerivedClass for T, but DerivedClass is not convertible to I<DerivedClass>, it is convertible to I<BaseClass>.

I don't know what you are trying to represent with the constraint that T be an I<T>. I do know that people often use this pattern to try to represent a constraint that the C# type system does not actually implement. See my article on the subject for details:

http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx

I encourage you to simplify things considerably; you seem to be trying to capture too much in the type system.

The reason why I<D> is not convertible to I<B> is because in order for variance to work, the interface must be marked as supporting variance; mark the T with out or in depending on whether you want covariance or contravariance.

However, since IList<T> is invariant, it will not be legal to make the derived interface covariant or contravariant. Consider IEnumerable<T> instead, as it is covariant in T.

In order for an interface to be covariant in T it needs to only use T in output positions. List<T> uses T in both input and output positions, so it cannot be covariant or contravariant.



回答2:

You need DerivedClass to be a ISortable<DerivedClass>:

public class DerivedClass : BaseClass, ISortable<DerivedClass>
{
    public new ISortableCollection<DerivedClass> ParentCollection
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }
}

Co- and contravariance on T cannot work here because you are deriving from IList<T> which is invariant.

Even removing IList<T> and removing the getter I can't get it to work right now with variance. Not exactly a strength of mine. This is a part of the type system that is better left alone if you can help it.

If the type system makes your head explode consider a dynamic solution:

((dynamic))item).ParentCollection = this;


回答3:

To thank you all i'm going to post the design i've end up with

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    public class SortableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>, ISortableCollection where T : ISortable, IComparable, IComparable<T>
    {
        public new void Add(T item)
        {
            if (this.Items.Contains(item))
                throw new InvalidOperationException("This list can contain the same item only once");
            base.Add(item);
        }

        public void Sort()
        {
            var sorted = this.Items.ToList();
            sorted.Sort();
            for (var i = 0; i < this.Items.Count; i++)
            {
                if (object.ReferenceEquals(this.Items[i], sorted[i]))
                {
                    this.Items[i].Index = i;
                    continue;
                }
                // if u want to support duplicates create a nextIndexOf and start searching from i
                var previousIndex = IndexOf(sorted[i]);
                Move(previousIndex, i);
            }
        }

        protected override void InsertItem(int index, T item)
        {
            item.Index = index;
            item.ParentCollection = this;
            base.InsertItem(index, item);
        }

        protected override void RemoveItem(int index)
        {
            this.Items[index].ParentCollection = null;
            base.RemoveItem(index);
        }

        protected override void ClearItems()
        {
            foreach (var item in this.Items)
                item.ParentCollection = null;
            base.ClearItems();
        }

        protected override void SetItem(int index, T item)
        {
            this.Items[index].ParentCollection = null;
            item.Index = index;
            item.ParentCollection = this;
            base.SetItem(index, item);
        }

        protected override void MoveItem(int oldIndex, int newIndex)
        {
            this.Items[oldIndex].Index = newIndex;
            this.Items[newIndex].Index = oldIndex;
            base.MoveItem(oldIndex, newIndex);
        }
    }

    public interface ISortableCollection : IList
    {
        void Sort();
    }

    public interface ISortable
    {
        Int32 Index { get; set; }
        ISortableCollection ParentCollection { get; set; }
    }

    public class BaseClass : ISortable, IComparable, IComparable<BaseClass>
    {
        public int Index { get; set; }

        public ISortableCollection ParentCollection { get; set; }

        public int CompareTo(object obj)
        {
            return CompareTo(obj as BaseClass);
        }

        public int CompareTo(BaseClass other)
        {
            if (other == null)
                return 1;
            return this.Index.CompareTo(other.Index);
        }
    }

    public class DerivedClass : BaseClass { }

    public class Controller
    {
        SortableCollection<BaseClass> MyBaseSortableList = new SortableCollection<BaseClass>();
        SortableCollection<DerivedClass> MyDerivedSortableList = new SortableCollection<DerivedClass>();

        public Controller()
        {
            //do things
            MyDerivedSortableList.Add(new DerivedClass());
            MyDerivedSortableList.Add(new DerivedClass());
            var derivedThing = new DerivedClass();
            MyDerivedSortableList.Add(derivedThing);
            var sibiling = derivedThing.ParentCollection[derivedThing.Index - 1] as BaseClass;  //way easier
            // switch the two objects order and call sort
            // calling a sort before the operation if indexes have been messed with
            // add an event to ISortable to notify the list the index has been changed and mark the list dirty
            derivedThing.Index -= 1;
            sibiling.Index += 1;
            derivedThing.ParentCollection.Sort();   // maybe the list was created where i couldn't access it
        }
    }
}