yazanpro
yazanpro

Reputation: 4772

List With Multiple Types

In C#, Is there a way to create a list that holds multiple types? My list items can be int, string, DateTime, or char. I'm aware of the use of List<object> or ArrayList but those are not good practice because of the encapsulation. Is there a good approach to achieve that? I thought that creating an interface might be helpful but I couldn't figure out a way.

Upvotes: 2

Views: 8480

Answers (4)

Aaron Anodide
Aaron Anodide

Reputation: 17196

I came up with a second but distinct approach (may be described in other answer but now implemented here) to this, this time leveraging implicit operations, and also with the ability to use the framework List type:

void Main()
{
    var list = new List<Item>
    {
        1,
        "foo",
        DateTime.Now,
        'x'
    };
    foreach (var item in list)
    {
        Console.WriteLine (item.ToString());
    }
    int i = list[0];
    string s = list[1];
    DateTime dt = list[2];
    char c = list[3];
    Console.WriteLine ("int: {0}, string: {1}, DateTime: {2}, char: {3}", i, s, dt, c);
}

enum Kind
{
    Int,
    String,
    DateTime,
    Char
}

class Item
{
    int intValue;
    string stringValue;
    DateTime dateTimeValue;
    char charValue;
    Kind kind;

    public object Value
    {
        get
        {
            switch (kind)
            {           
                case Kind.Int:
                    return intValue;
                case Kind.String:
                    return stringValue;
                case Kind.DateTime:
                    return dateTimeValue;
                case Kind.Char:
                    return charValue;
                default:
                    return null;
            }

        }
    }

    public override string ToString()
    {   
        return Value.ToString();
    }

    // Implicit construction
    public static implicit operator Item(int i)
    {
        return new Item { intValue = i, kind = Kind.Int };
    }
    public static implicit operator Item(string s)
    {
        return new Item { stringValue = s, kind = Kind.String };
    }
    public static implicit operator Item(DateTime dt)
    {
        return new Item { dateTimeValue = dt, kind = Kind.DateTime };
    }
    public static implicit operator Item(char c)
    {
        return new Item { charValue = c, kind = Kind.Char };
    }

    // Implicit value reference
    public static implicit operator int(Item item)
    {
        if(item.kind != Kind.Int) // Optionally, you could validate the usage
        {
            throw new InvalidCastException("Trying to use a " + item.kind + " as an int");
        }
        return item.intValue;
    }
    public static implicit operator string(Item item)
    {
        return item.stringValue;
    }
    public static implicit operator DateTime(Item item)
    {
        return item.dateTimeValue;
    }
    public static implicit operator char(Item item)
    {
        return item.charValue;
    }
}

Upvotes: 3

KeithS
KeithS

Reputation: 71591

IConvertible, IIRC, is implemented by all those types, so you could accept a List<IConvertible>. Other types implement IConvertible as well but not as many as object (which is of course almost all types in .NET). This gives you the advantage of using the built-in IConvertible methods to switch between types as desired.

Another easy way is List<string> and then just ToString() all of these (IConvertible forces implementation of a culture-specific ToString() overload, and all built-in IConvertibles also override Object.ToString()). You'll take a performance hit as string conversion and parsing is relatively expensive and you'll have to try parsing the string to each primitive before figuring out it's actually just a string, so I wouldn't recommend this for large lists that have to be efficiently processed. There's also potential for loss of true type; if you add the string "12345" to the list, the parser will think it was originally an int, though what you passed was only ever a string.

I think a wrapper is your best bet. The best built-in one is Tuple; you could define a List<Tuple<int,string,DateTime,char,int>> where the last int parameter identifies the populated field of the Tuple (1=int, 2=string, etc), and all others will have their default value. You can use Item5 to dynamically retrieve Item1 through Item4, or just put it in a switch statement. You can wrap the list and all this tuple packing and unpacking in extension methods operating on the list, or by deriving or containing the list in another class.

As a hybrid of the previous two approaches, you can encapsulate the type information and the string representation: Tuple<string, ElementType> (where ElementType is an enum representing possible types the string representation originally could have been) which eliminates the guesswork; just parse back to the type specified.

Upvotes: 1

Aaron Anodide
Aaron Anodide

Reputation: 17196

Like the other poster said, in your case I think List<object> is probably fine, but I decided to see if I could come up with something/anything so here's a "non-real-world" (i.e. there's no good reason to do this so please don't paste it into your production code) example for you:

void Main()
{
    var items = new Items();
    items.Add(1);
    items.Add("foo");
    items.Add(DateTime.Now);
    items.Add('x');

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

enum Kind
{
    Int,
    String,
    DateTime,
    Char
}

class Items : IEnumerable<object>
{
    Stack<int> Ints = new Stack<int>();
    Stack<string> Strings = new Stack<string>();
    Stack<DateTime> DateTimes = new Stack<DateTime>();
    Stack<char> Chars = new Stack<char>();
    List<Kind> Kinds = new List<Kind>();

    IEnumerator<object> System.Collections.Generic.IEnumerable<object>.GetEnumerator()
    {
        foreach (var kind in Kinds)
        {
            switch (kind)
            {
                case Kind.Int:
                    yield return Ints.Pop();
                    break;
                case Kind.String:
                    yield return Strings.Pop();
                    break;
                case Kind.DateTime:
                    yield return DateTimes.Pop();
                    break;
                case Kind.Char:
                    yield return Chars.Pop();
                    break;
            }
        }
    }

    IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return (this as System.Collections.Generic.IEnumerable<object>).GetEnumerator();
    }

    public void Add(int i)
    {
        Ints.Push(i);
        Kinds.Add(Kind.Int);
    }

    public void Add(string s)
    {
        Strings.Push(s);
        Kinds.Add(Kind.String);
    }

    public void Add(DateTime dt)
    {
        DateTimes.Push(dt);
        Kinds.Add(Kind.DateTime);
    }

    public void Add(char c)
    {
        Chars.Push(c);
        Kinds.Add(Kind.Char);
    }
}

Upvotes: 2

Luiso
Luiso

Reputation: 4113

Your best choice is either make a List<object> and cast it every time or make a wrapper like this:

class Either<TRight, TLeft> 
{
    private TLeft Left {get; private set;}
    private TRight Right {get; private set;}
    private bool IsLeft {get; private set;}        
}

I didn't add the constructors or anything else needed, I hope that's straight forward enough. I got the idea from F# and functional languages, you can check that later if you want to improve it.

Then make a List<Either<DateTime,char>> and ask in every case if it's a DateTime or char and work your way around that

Upvotes: 1

Related Questions