简体   繁体   中英

C# Passing current generic class instance to other class

lately I started to learn generics. I run into trouble with storing references to generic classes instances. As you can see, my class ListHandler can store references to specific type of BaseClass. I would love to register BaseClass instances by themselves, which is why I wanted to guarantee that they will use BaseParamClass by adding 'where'. Anyway - it does not compile.'This', does not know that T is actually BaseClassParam even with 'where' keyword in class. I don't know what is wrong here and I couldn't find answer anywhere. I would be grateful for tips/guides/solutions.

public class ListHandler
{
    private List<BaseClass<BaseParamClass>> list;

    public ListHandler()
    {
        list = new List<BaseClass<BaseParamClass>>();
    }

    public void Register(BaseClass<BaseParamClass> param)
    {
        list.Add(param);
    }
}

public class BaseClass<T> where T : BaseParamClass
{
    private ListHandler listHandler;

    public T Param { get; private set; }

    public BaseClass(ListHandler listHandler)
    {
        this.listHandler = listHandler;
        listHandler.Register(this); //throws error
    }
}

Why don't you make ListHandler generic as well?

public class ListHandler<T>
{
    private List<BaseClass<T>> list;

    public ListHandler()
    {
        list = new List<BaseClass<T>>();
    }

    public void Register(BaseClass<T> param)
    {
        list.Add(param);
    }
}

public class BaseClass<T> 
{
    private ListHandler<T> listHandler;

    public T Param { get; private set; }

    public BaseClass(ListHandler<T> listHandler)
    {
        this.listHandler = listHandler;
        listHandler.Register(this); 
    }
}

Also, it seems strange to me to have BaseClass<T> contain a reference to a class that has a reference to BaseClass<T> itself.

Why your code doesn't compile

In order to fully understand why your code doesn't compile, you'll have to dive into covariance and contravariance , which is a big topic and hard to explain in an SO answer. It can be especially confusing if you've gotten to a point where inheritance polymorphism is second nature to you; the rules are just different enough to be make your head hurt.

Here is what is confusing--

You're used to doing this:

object a = new String(...);

But generics don't let you do this!

List<object> c = new List<string>();  //Compiler error

That's because those two Lists are not related the same way that object and string are related. One does not inherit from the other. Rather, they are different variants of a generic type definition. In the generic world, you can't assign one to the other. The same is true of this:

void Foo<T>() where T: BaseParamClass 
{
    BaseClass<BaseParamClass> a = new BaseClass<T>(); //Compiler error
}

In this example, T could be BaseParamClass or one of its derived types. They are not the same type. So to remain type-safe, the compiler has to disallow this assignment, and your Register call, which has the same type mismatch.

Standard ways around this

You need a covariant interface. These allow assignment from derived to base. So for example, while this is still illegal:

List<object> a = new List<string>();  //Compiler error

This is totally fine:

IEnumerable<object> e = new List<string>();  //Is OK

Because IEnumerable was declared to be covariant, like this:

interface IEnumerable<out T> 

Which means it is can be assigned in this way. It works because using out also adds a compiler constraint to the interface: it can be used to retrieve stuff...

interface IEnumerable<out T> 
{
    T Item[int index];
}

...but it cannot accept anything:

interface IEnumerable<out T> 
{
    Add(T item); //Compiler error
}

These constraints are what allow generics to provide early-bound type safety while still allowing certain forms of (non-inheritance) polymorphism.

What I'd suggest

Based on your comment, it sounds like you just need a container (a stack, apparently) that can hold references to these BaseClass<T> instances. If you are following separation of concerns, the stack doesn't need to actually do anything with the T , other than store it and retrieve it, and to allow it to register itself.

Since that is a separate concern, make a separate interface.

And in the interest of keeping things simple, maybe avoid using generics completely for this bit.

One way to do it--

Create an interface that allows access to everything the stack needs to know about an item it is containing. For example, if the stack contains popups of various kinds, you may want to expose the popup's title.

interface IStackable
{
    string Title { get; set; }
}

Now use it like this:

public class ListHandler 
{
    private readonly Dictionary<string, IStackable> list;

    public ListHandler()
    {
        list = new Dictionary<string, IStackable>();
    }

    public void Register(IStackable item)
    {
        list.Add(item.Title, item);
    }
}

public class BaseClass<T> : IStackable where T : BaseParamClass
{
    private ListHandler listHandler;

    public T Param { get; private set; }

    public BaseClass(ListHandler listHandler)
    {
        this.listHandler = listHandler;
        listHandler.Register(this);
    }

    public string Title { get; set; }
}

Unless there is some other requirement, you shouldn't need to make it any more complicated than that.

All you really need to do is add an interface. This works:

public class BaseParamClass
{
}

public class ListHandler 
{
    private List<IBase<BaseParamClass>> list;

    public ListHandler()
    {
        list = new List<IBase<BaseParamClass>>();
    }

    public void Register(IBase<BaseParamClass> param)
    {
        list.Add(param);
    }
}

public interface IBase<T> where T : BaseParamClass
{
    T Param {get; }
}

public class BaseClass : IBase<BaseParamClass>
{
    private ListHandler listHandler;

    public BaseParamClass Param { get; private set; }

    public BaseClass(ListHandler listHandler)
    {
        this.listHandler = listHandler;
        listHandler.Register(this); 
    }
}

Working code on DotNetFiddle

I have another option for you.

Let's split the BaseClass<T> class into two with a non-generic base, like so:

public class BaseClass
{
    protected ListHandler listHandler;

    public BaseClass(ListHandler listHandler)
    {
        this.listHandler = listHandler;
    }
}

public class BaseClass<T> : BaseClass where T : BaseParamClass
{

    public T Param { get; private set; }

    public BaseClass(ListHandler listHandler)
        : base(listHandler)
    {
        listHandler.Register(this); // Compiles nicely! Yay!
    }
}

Now, the list inside ListHandler can be defined as private List<BaseClass> list; . That means there is no problem adding any BaseClass item to the list. We also can then define two methods for registering and fetching generic versions of the BaseClass<T> from the ListHandler . It would look like this:

public class ListHandler
{
    private List<BaseClass> list;

    public ListHandler()
    {
        list = new List<BaseClass>();
    }

    public void Register<T>(BaseClass<T> param) where T : BaseParamClass
    {
        list.Add(param);
    }

    public BaseClass<T> Fetch<T>() where T : BaseParamClass
    {
        return list.Select(x => x as BaseClass<T>).Where(x => x != null).FirstOrDefault();
    }
}

So, given a class public class FooParam : BaseParamClass { } I can write this code:

ListHandler listHandler = new ListHandler();
BaseClass<FooParam> baseClass = new BaseClass<FooParam>(listHandler);
BaseClass<FooParam> baseClass2 = listHandler.Fetch<FooParam>();

Console.WriteLine(object.ReferenceEquals(baseClass, baseClass2));

The result from this code is True is written to the console - which means I can successfully fetch the instance of BaseClass<FooParam> from the ListHandler .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM