简体   繁体   中英

Collection of generic types

If I have a generic class:

public class MyClass<T> 
{
  public T Value;
}

I want to instantiate several items such as...

new MyClass<string>
new MyClass<int>

...and add them to a collection. How do I define the collection so that it can hold a list of generic types? I then want to iterate through the collection at some point, and use the Value property. Possible?

Have your generic class inherit from a non-generic base, or implement a non-generic interface. Then you can have a collection of this type and cast within whatever code you use to access the collection's contents.

Here's an example.

public abstract class MyClass
{
    public abstract Type Type { get; }
}

public class MyClass<T> : MyClass
{
    public override Type Type
    {
        get { return typeof(T); }
    }

    public T Value { get; set; }
}

// VERY basic illustration of how you might construct a collection
// of MyClass<T> objects.
public class MyClassCollection
{
    private Dictionary<Type, MyClass> _dictionary;

    public MyClassCollection()
    {
        _dictionary = new Dictionary<Type, MyClass>();
    }

    public void Put<T>(MyClass<T> item)
    {
        _dictionary[typeof(T)] = item;
    }

    public MyClass<T> Get<T>()
    {
        return _dictionary[typeof(T)] as MyClass<T>;
    }
}

The only way I can think of, off the top of my head is as follows (wrapped up in a Console app for testing):

class Program
{
    static void Main(string[] args)
    {
        var x = new MyClass<string>() { Value = "34" };
        var y = new MyClass<int>() { Value = 3 };

        var list = new List<IMyClass>();
        list.Add(x);
        list.Add(y);

        foreach (var item in list)
        {
            Console.WriteLine(item.GetValue);
        }
    }

    private interface IMyClass
    {
        object GetValue { get; }
    }

    private class MyClass<T> : IMyClass
    {
        public T Value;

        public object GetValue
        {
            get
            {
               return Value;
            }
        }
    }
}

ie Have MyClass implement an empty interface and then create your collections as one that holds instances of classes that implement that interface.

Update: I've added a "GetValue" method to the interface that allows you to access the "Value" of the MyClass instance as an Object. This is about as good as it's going to get, afaik, if you want to have a collection that holds mixed types.

I have interfaces on most of my generic types with "Untyped" members:

private interface IMyClass
{
    object UntypedValue { get; }
}

private class MyClass<T> : IMyClass
{
    T Value { get; set; }

    object UntypedValue { get { return Value; } }
}

You could also do this by the use of explicit interface implementation, but in my opinion, it is much easier by using a separate name. (There are some CA hints on explicit interface implementation as well)

You'll want to define a base class for MyClass, then your collections will be a List of base class. Ex:

void Main()
{
 var a = new MyClass<string>();
 var b = new MyClass<int>();
 var c = new List<MyBase>();
 c.Add(a);
 c.Add(b);

}

public class MyBase { }
// Define other methods and classes here
public class MyClass<T> : MyBase {
public T Value { get; set;}
}

You want to have a collection of MyClass for which the value of the type parameter T is different in each instance. This is not possible in .NET; it lacks the equivalent of the Java wildcard (?). What you need to do instead is create a non-generic base class or interface, which MyClass can implement. For example:

public interface IMyClass {
  object Value { get; set; }
}
public class MyClass<T> : IMyClass {
  public T Value { get; set; }
  object IMyClass.Value {
    get { return Value; }
    set { Value = (T)value; }
  }
}
List<IMyClass> m = new List<IMyClass> { new MyClass<string>(), new MyClass<int> };

Since .Net 3 there has been a CompositeCollection class which allows multiple unique items or even collections to be contained within. It is used by WPF developers to store and display differing items in Xaml. But that doesn't mean it can't be used in non WPF situations.

Here is an example where I store differing things from strings to decimals and extract and enumerate over all items, then items of a specific type:

CompositeCollection cc = new CompositeCollection();

cc.Add(1);
cc.Add("Item One");
cc.Add(1.0M);
cc.Add(1.0);
cc.Add("Done");

Console.WriteLine ("Every Item");

foreach (var element in cc)
    Console.WriteLine ("\t" + element.ToString());

Console.WriteLine (Environment.NewLine + "Just Strings");

foreach (var str  in cc.OfType<string>())
    Console.WriteLine ("\t" + str);

/* Outputs:

Every Item
  1
  Item One
  1.0
  1
  Done

Just Strings
  Item One
  Done

*/

I think the problem here is that generic classes really aren't the same type at all. They're simply templates which create entire new types at compile time (if I understand correctly). Therefore, MyClass<int> and MyClass<string> are completely different types, according to the runtime. They might as well be MyIntClass and MyStringClass , which you obviously cannot have in the same list without boxing them first. They don't (necessarily) inherit the same base class, implement the same interfaces, or anything else. They're as different as any other two types out there, and you have to treat them as such (even though you think you know better).

Of course, you can have them implement an interface, inherit a base object, or any of the other options already given. Take a look at commongenius' answer for a good way to do this.

I believe your collection would all have to be of the same MyClass type (as in T would have to be the same), because the compiler won't know what types you'd added to which elements in the collection.

In other words, if you were to add 2 elements to a list:

list.Add(new MyClass<string>());
list.Add(new MyClass<int>());

then try to reference one:

var myItem = list[1];

The compiler doesn't know what generic was assigned to the MyClass list at element 1 , because the elements are added at runtime, but the generics are determined at compile time.

I'm pretty sure that what you want to do can't be done.


If you know the number of elements in advance, perhaps you could use a Tuple ?

IList

There is no way to define a generic collection that can accept any flavor of your generic class... ie IList<MyClass> . Generic classes are only a short cut for the developer to save on writing a bunch of repetitive code but at compile time each flavor of the generic class is translated into a concrete. ie if you have MyClass<string> , MyClass<int> , MyClass<bool> then the compiler will generate 3 seperate and distinct classes. The only way to do what you want is to have an interface for your generic.

public interface IMyGeneric {
    Type MyType { get; set;}    
}

class MyGeneric<T> : IMyGeneric {

    public MyGeneric() {
        MyType = typeof(T);
    }

    public Type MyType {
        get; set;
    }
}

and then you can say

IList<IMyGeneric> list = new List<IMyGeneric>();
list.add(new MyGeneric<string>());
list.add(new MyGeneric<int>());

Another approach is to use a non-generic class, and pass a handler to it to perform the action "from the inside".

interface IMyClass
{
    void ShowValues(MyHandler handler);
}

// Contains a List<string>.
class MyClassString : IMyClass
{
    private List<string> _list = new List<string> { "hello", "world" };

    public void ShowValues(MyHandler handler)
    {
        handler.ShowValues(_list);

    }
}

// Contains a List<int>.
class MyClassInt : IMyClass
{
    private List<int> _list = new List<int> { 1, 2 };

    public void ShowValues(MyHandler handler)
    {
        handler.ShowValues(_list);
    }
}

// Handler that has overloaded methods for all the supported types.
class MyHandler
{
    public void ShowValues(IEnumerable<string> list)
    {
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
    }

    public void ShowValues(IEnumerable<int> list)
    {
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var myClasses = new IMyClass[]
        {
            new MyClassString(),
            new MyClassInt(),
        };

        var myHandler = new MyHandler();

        foreach (var myClass in myClasses)
        {
            myClass.ShowValues(myHandler);
        }
    }
}

Sorry I'm late to the party but I want the points. The trick to this whole process is to think of types "int" and "string" as classes. Thus the trick is to have a list<MyClass<object>>.

Now, you say, how do I get the class to produce objects of any use or provide type checking on my inputs to the object? Well you can't. Because an object is a dumb base level element and the compiler really can't do much with it. However, the solution to your problem is based around the same idea.

Instead of classes int and string, what if you created classes MyInt and MyString? Well now, similar to int and string, you could extend those classes to implement a common interface like IMyClassType. Update MyClass to public class MyClass<T> where T:IMyClassType. Now, replace your instances of MyClass<int> with Myclass<MyInt> and MyClass<MyString>.

Finally, after all of that, you can replace your useless List<MyClass<object>> with a useful List<MyClass<IMyClassType>>, which has the ability to hold MyClass instances containing properties and methods referencing the IMyClassTypes you've created.

This design requirement of C# is related to a pattern called the Generic Adapter Pattern , and once you get it, it'll make designing complex objects with interdependent object types a breeze.

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