Search code examples
c#.netgenericscovariancecontravariance

How can I fix this really simple .NET code using Generics to return the strongly typed result?


NOTE: All of the code below exists in this .NET Fiddle.

I'm having trouble (again) trying to grok Generics/covariance/contravariance. :(

It's so killing me - so please be kind to me.

So, I have this fictitious pet shop and I'm trying to easily search for a Pet in our pet-inventory.

The main search class SearchMagic is where all my greif is. I'm trying to say the following:

var pet = searchMagic.SearchForAPet("Fido"); and it will .. in this case return a Dog which is Fido.

Now the kicker is how I've implemented the class SearchMagic. This is where it has the smarts to search for Cat's first, then Dog's. The problem is, I just can't seem to figure out a way to return the cat or dog instance, if one was found. It keeps complaining about Cannot implicitly convert...

I keep trying to say to the code: At the very least, I need a SearchResult<IAnimal> returned. So if that's a concrete Cat or Dog, then awesome-sauce. Otherwise, null. And then for the code that calls that method and gets the returned result, the only things I can do on that result, are anything defined in SearchResult<IAnimal>. So if my Cat instance has some cat specific properties or methods, then bad luck, I can't call them on the result because the result is an SearchResult<IAnimal> and only the IAnimal stuff is exposed.

Here's the code, with some comments INLINE to show where the error occurs...

Show me ze code, please.

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");

        var searchMagic = new SearchMagic();

        var animal = searchMagic.SearchForAPet("Fido");
    }


    // ***********************************
    // ** Interfaces / contracts        **
    // ***********************************


    public interface IAnimal
    {
        string Name { get; set; }
    }

    public interface ISearchService<TAnimal> where TAnimal : IAnimal
    {
        SearchResult<TAnimal> SearchInventory(string query);
    }

    public class SearchResult<TAnimal> where TAnimal : IAnimal
    {
        TAnimal Pet { get; set; }
        decimal Price { get; set; }
    }


    // ***********************************
    // ** Concrete implementations      **
    // ***********************************

    public class Cat : IAnimal
    {
        public string Name { get; set; }
    }

    public class Dog : IAnimal
    {
        public string Name { get; set; }
    }

    public class CatSearchService : ISearchService<Cat>
    {
        public SearchResult<Cat> SearchInventory(string query)
        {
            return new SearchResult<Cat>();
        }
    }

    public class DogSearchService : ISearchService<Dog>
    {
        public SearchResult<Dog> SearchInventory(string query)
        {
            return new SearchResult<Dog>();
        }
    }

    public class SearchMagic
    {
        private CatSearchService _catSearchService = new CatSearchService();
        private DogSearchService _dogSearchService = new DogSearchService();

        public SearchResult<IAnimal> SearchForAPet(string query)
        {
            var cat = _catSearchService.SearchInventory(query);
            if (cat != null)
            {
                // ** ERROR: Cannot implicitly convert type 'Program.SearchResult<Program.Cat>' to 'Program.SearchResult<Program.IAnimal>'
                return cat;
            }

            var dog = _dogSearchService.SearchInventory(query);
            if (dog != null)
            {
                // ** ERROR: Cannot implicitly convert type 'Program.SearchResult<Program.Dog>' to 'Program.SearchResult<Program.IAnimal>'  
                return dog;
            }

            return null;
        }
    }

}

Solution

  • Here's one way of doing it.

    If you want your SearchForAPet method to be able to return a "search result" that could either be a "dog search result" or a "cat search result", it needs to return a covariant interface, not a concrete type:

    public interface ISearchResult<out TAnimal> where TAnimal: IAnimal
    {
        TAnimal Pet { get; }
        decimal Price { get; }
    }
    
    public class SearchResult<TAnimal> : ISearchResult<TAnimal> where TAnimal : IAnimal
    {
        public TAnimal Pet { get; set; }
        public decimal Price { get; set; }
    }
    
    ...
    
    public ISearchResult<IAnimal> SearchForAPet(string query)
    {
        ...
    }