5

I am trying to learn how to create generic classes with c#. Can someone explain why I get a compile error when I run this program.

I have created the IZooAnimal interface. All zoo animals will implement this interface.

public interface IZooAnimal
{
    string Id { get; set; }
}

public class Lion : IZooAnimal
{
    string Id { get; set; }
}

public class Zebra : IZooAnimal
{
    public string Id { get; set; }
}

The ZooCage will hold animals of the same Type

public class ZooCage<T> where T : IZooAnimal
{
    public IList<T> Animals { get; set; }
}

The zoo class have cages

public class Zoo
{
    public IList<ZooCage<IZooAnimal>> ZooCages { get; set; }
}

The program that uses the classes

class Program
{
    static void Main(string[] args)
    {
        var lion = new Lion();
        var lionCage = new ZooCage<Lion>();
        lionCage.Animals = new List<Lion>();
        lionCage.Animals.Add(lion);

        var zebra = new Zebra();
        var zebraCage = new ZooCage<Zebra>();
        zebraCage.Animals = new List<Zebra>();
        zebraCage.Animals.Add(zebra);

        var zoo = new Zoo();
        zoo.ZooCages = new List<ZooCage<IZooAnimal>>();

       zoo.ZooCages.Add(lionCage);
    }
}

When I compile I get the following error: Error 2 Argument 1: cannot convert from 'ConsoleApplication2.ZooCage<ConsoleApplication2.Lion>' to 'ConsoleApplication2.ZooCage<ConsoleApplication2.IZooAnimal>'

What changes do I have to do in order to make my program run?

Alessandro D'Andria
  • 8,663
  • 2
  • 36
  • 32

3 Answers3

4

@DanielMann's answer is quite good, but suffers from one drawback: the original IList interface cannot be used with the ICage interface. Instead, the ICage has to expose a ReadOnlyCollection, and expose a new method called CageAnimal.

I've also re-written the code using a similar approach. My ICage implementation is much weaker, but it allows you to stick with IList semantics inside.

public interface IZooAnimal
{
    string Id { get; set; }
}

public class Lion : IZooAnimal
{
    public string Id { get; set; }
}

public class Zebra : IZooAnimal
{
    public string Id { get; set; }
}

public interface ICage
{
    IEnumerable<IZooAnimal> WeaklyTypedAnimals { get; }
}

public class Cage<T> : ICage where T : IZooAnimal
{
    public IList<T> Animals { get; set; }

    public IEnumerable<IZooAnimal> WeaklyTypedAnimals
    {
        get { return (IEnumerable<IZooAnimal>) Animals; }
    }
}

public class Zoo
{
    public IList<ICage> ZooCages { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var lion = new Lion();
        var lionCage = new Cage<Lion>();
        lionCage.Animals = new List<Lion>();
        lionCage.Animals.Add(lion);

        var zebra = new Zebra();
        var zebraCage = new Cage<Zebra>();
        zebraCage.Animals = new List<Zebra>();
        zebraCage.Animals.Add(zebra);

        var zoo = new Zoo();
        zoo.ZooCages = new List<ICage>();

        zoo.ZooCages.Add(lionCage);
    }
}
CSJ
  • 3,641
  • 2
  • 19
  • 29
2

You should define your lists not with the concrete type that implements the interface, but with the interface:

    var lionCage = new ZooCage<IZooAnimal>();
    lionCage.Animals = new List<IZooAnimal>();

Then your code will work as expected.

The initial code did not work, because it is not allowed to convert concrete types to a generalised type (as @default.kramer pointed out covariance and contravariance).

The solution that i came up is following:

// your ZooCage is still generic
public class ZooCage<T>
{   
    // but you declare on creation which type you want to contain only!
    private Type cageType = null;
    public ZooCage(Type iMayContain)
    {
        cageType = iMayContain;
        animals = new List<T>();
    }
    // check on add if the types are compatible
    public void Add(T animal)
    {
        if (animal.GetType() != cageType)
        {
            throw new Exception("Sorry - no matching types! I may contain only " + cageType.ToString());
        }
        animals.Add(animal);
    }
    // should be generic but not visible to outher world!
    private IList<T> animals { get; set; }
}

This code allows you to do:

    var lion = new Lion();
    var lionCage = new ZooCage<IZooAnimal>(typeof(Lion));
    lionCage.Add(lion);

    var zebra = new Zebra();
    var zebraCage = new ZooCage<IZooAnimal>(typeof(Zebra));
    zebraCage.Add(zebra);

But it will throw an error on:

    zebraCage.Add(lion);

Now the zoo can be safely extended.

keenthinker
  • 7,645
  • 2
  • 35
  • 45
  • 2
    This will prevent the compile error, but it defeats the whole purpose of using generics. May as well not make ZooCage generic at all if you're going to do this. It also does not fulfull the requirement "The ZooCage will hold animals of the same Type", because all the cages will allow animals of any type. – JLRishe Aug 24 '13 at 17:33
  • The problem is that what the OP wants is not possible, not just because the compiler says so, but because compile-time type safety verification would go out the window. The compiler would not be able to verify that the code is safe if you allowed that syntax, so you would be back to runtime checks for everything. So it doesn't matter how you dice this, it's not possible because it's not a good idea. – Lasse V. Karlsen Aug 24 '13 at 17:39
  • How should I design the Zoo and the Zoocage classes? I want the lion cage to hold lions and lions only. And I want the Zoocage to be able to grow with more cages. – user2714000 Aug 24 '13 at 17:54
2

Since you want to have multiple cages, but each type of cage can only hold one animal, your model is slightly off.

I rewrote the code as follows:

  • IZooAnimal is unchanged.
  • There's a covariant interface ICage that accepts any type of IZooAnimal. That allows you to have a strongly-typed cage for every type of animal.
  • Then, I have a Cage concrete implementation of ICage. Cage is generic, but you could just as easily make it an abstract class and then make animal-specific cage implementations. For example, if your zebra needs to be fed grass, and your lion needs to be fed meat, you could specialize the implementations of their cages.

Here's the complete code:

public interface IZooAnimal
{
    string Id { get; set; }
}

public interface ICage<out T> where T : IZooAnimal
{
    IReadOnlyCollection<T> Animals { get; }
}

public class Cage<T> : ICage<T> where T: IZooAnimal
{
    private readonly List<T> animals = new List<T>();

    public IReadOnlyCollection<T> Animals
    {
        get
        {
            return animals.AsReadOnly();
        }
    }

    public void CageAnimal(T animal)
    {
        animals.Add(animal);
    }
}

public class Lion : IZooAnimal
{
    public string Id { get; set; }
}

public class Zebra : IZooAnimal
{
    public string Id { get; set; }
}

public class Zoo
{
    public IList<ICage<IZooAnimal>> Cages { get; set; }
}

internal class Program
{

    private static void Main(string[] args)
    {
        var lion = new Lion();
        var zebra = new Zebra();
        var lionCage = new Cage<Lion>();
        lionCage.CageAnimal(lion);

        var zebraCage = new Cage<Zebra>();
        zebraCage.CageAnimal(zebra);

        var zoo = new Zoo();
        zoo.Cages.Add(lionCage);
        zoo.Cages.Add(zebraCage);

    }
}
Daniel Mann
  • 57,011
  • 13
  • 100
  • 120