How do I pattern match on a generic with an abstract inner type in C#?

Viewed 156

I'd like to apply switch statement pattern matching to determine if I'm dealing with a specific generic type. The inner type may be one of several that inherit from an abstract class. Here's an example:

using System;

namespace ConsoleApp1
{
  abstract class Vehicle
  {
    public abstract string Name { get; }
  }

  internal class Sedan : Vehicle
  {
    public override string Name => "Sedan";
  }

  abstract class AbstractLoad
  {
    public abstract string Name { get; }
  }

  internal class Oil : AbstractLoad
  {
    public override string Name => "Oil";
  }

  internal class Lumber : AbstractLoad
  {
    public override string Name => "Lumber";
  }

  class Semi<TLoad> : Vehicle where TLoad : AbstractLoad
  {
    public override string Name => "Semi";

    public TLoad Load { get; set; }
  }
  class Program
  {
    static void Main(string[] args)
    {
      var vehicle = new Semi<Oil> {Load = new Oil()};

      DescribeVehicle(vehicle);
    }

    static void DescribeVehicle(Vehicle vehicle)
    {
      switch (vehicle)
      {
        case Sedan sedan:
          Console.WriteLine("This vehicle is a sedan");
          break;
        case Semi<AbstractLoad> semi:
          Console.WriteLine($"This vehicle is a semi carrying {semi.Load.Name}");
          break;
        default:
          Console.WriteLine("We don't know what kind of vehicle this is.");
          break;
      }
    }
  }
}

I'd like it to print, "This vehicle is a semi carrying Oil". However, it prints, "We don't know what kind of vehicle this is." Is there some way I can get it to match the Semi case?

3 Answers

Think about what the type information for vehicle contains: vehicle is indeed a ConsoleApp1.Semi<ConsoleApp1.Oil> and not a ConsoleApp1.Semi<ConsoleApp1.AbstractLoad>, right? That's why the type doesn't match.

You have several possibilities here:

  1. You can obviously use multiple case branches, each with exact types, but it's bad style because it defeats the purpose and makes you write a lot of duplicate code, and there's a high risk of forgetting to add the new branch when you create a new sub-class of AbstractLoad. Do this only if you really have no other choice, but it should give you massive headache whenever you need this. Avoid it if possible, it's bad style. No, really.
// === DISCLAIMER ===
// Just for illustrative purposes.
// Don't use this solution, it's bad style.

using System;

namespace ConsoleApp1
{
    abstract class Vehicle
    {
        public abstract string Name { get; }
    }

    internal class Sedan : Vehicle
    {
        public override string Name => "Sedan";
    }

    abstract class AbstractLoad
    {
        public abstract string Name { get; }
    }

    internal class Oil : AbstractLoad
    {
        public override string Name => "Oil";
    }

    internal class Lumber : AbstractLoad
    {
        public override string Name => "Lumber";
    }

    class Semi<TLoad> : Vehicle where TLoad : AbstractLoad
    {
        public override string Name => "Semi";

        public TLoad Load { get; set; }
    }
    class Program
    {
        static void Main(string[] args) {
            var vehicle = new Semi<Oil> { Load = new Oil() };

            DescribeVehicle(vehicle);
        }

        static void DescribeVehicle(Vehicle vehicle) {
            switch (vehicle) {
                case Sedan _:
                    Console.WriteLine("This vehicle is a sedan");
                    break;
                case Semi<Oil> _:
                    Console.WriteLine("This vehicle is a semi carrying Oil.");
                    break;
                case Semi<Lumber> _:
                    Console.WriteLine("This vehicle is a semi carrying Lumber.");
                    break;
                // Add new cases here (but rather don't use this solution at all)
                default:
                    Console.WriteLine("We don't know what kind of vehicle this is.");
                    break;
            }
        }
    }
}
  1. Instead of using one DescribeVehicle(Vehicle vehicle) method with a switch, you can create a second DescribeVehicle<TLoad>(Semi<TLoad> semi) where TLoad : AbstractLoad method. This is a moderate duplication of code, and less dangerous, but still not super-pretty.
using System;

namespace ConsoleApp1
{
    abstract class Vehicle
    {
        public abstract string Name { get; }
    }

    internal class Sedan : Vehicle
    {
        public override string Name => "Sedan";
    }

    abstract class AbstractLoad
    {
        public abstract string Name { get; }
    }

    internal class Oil : AbstractLoad
    {
        public override string Name => "Oil";
    }

    internal class Lumber : AbstractLoad
    {
        public override string Name => "Lumber";
    }

    class Semi<TLoad> : Vehicle where TLoad : AbstractLoad
    {
        public override string Name => "Semi";

        public TLoad Load { get; set; }
    }
    class Program
    {
        static void Main(string[] args) {
            var vehicle = new Semi<Oil> { Load = new Oil() };

            DescribeVehicle(vehicle);
            Console.ReadKey();
        }

        static void DescribeVehicle(Vehicle vehicle) {
            switch (vehicle) {
                case Sedan _:
                    Console.WriteLine("This vehicle is a sedan");
                    break;
                default:
                    Console.WriteLine("We don't know what kind of vehicle this is.");
                    break;
            }
        }

        static void DescribeVehicle<TLoad>(Semi<TLoad> semi) where TLoad : AbstractLoad {
            Console.WriteLine($"This vehicle is a semi carrying {semi.Load.Name}");
        }
    }
}

The real deal is that your approach of using an independent static function with a switch statement is already part of the problem:

  1. Since you're already using inheritance, use the ToString() method. Override it in the Sedan or Semi<TLoad> class with the output you want, and ideally do the same for the Oil/Lumber etc. classes. Or, if you need ToString() for something else, then create your own abstract base method string Describe() in Vehicle and AbstractLoad and override that in Sedan/Oil etc.
using System;

namespace ConsoleApp1
{
    abstract class Vehicle
    {
        public abstract string Name { get; }

        public override string ToString() => $"This vehicle is a {Name}.";

        // To keep the output identical to your example, use this method

        // public override string ToString() => $"We don't know what kind of vehicle this is.";

        // You then have to override ToString in Sedan, too. But I think there's no reason
        // why you'd want to have to override ToString everywhere with the same code.
    }

    internal class Sedan : Vehicle
    {
        public override string Name => "Sedan";
    }

    abstract class AbstractLoad
    {
        public abstract string Name { get; }

        public override string ToString() => Name;
    }

    internal class Oil : AbstractLoad
    {
        public override string Name => "Oil";
    }

    internal class Lumber : AbstractLoad
    {
        public override string Name => "Lumber";
    }

    class Semi<TLoad> : Vehicle where TLoad : AbstractLoad
    {
        public override string Name => "Semi";

        public TLoad Load { get; set; }

        public override string ToString() => $"This vehicle is a semi carrying {Load}.";
    }
    class Program
    {
        static void Main(string[] args) {
            var vehicle = new Semi<Oil> { Load = new Oil() };

            DescribeVehicle(vehicle);
        }

        static void DescribeVehicle(Vehicle vehicle) {
            Console.WriteLine(vehicle.ToString());
        }
    }
}

Update:

I like the second solution from LWChris

It does work well:

class Program
{
    static void Main(string[] args)
    {
        var vehicle = new Semi<Oil> { Load = new Oil() };
        DescribeVehicle<Oil>(vehicle);
    }

    static void DescribeVehicle<TLoad>(Semi<TLoad> vehicle) where TLoad : AbstractLoad
    {
        switch (vehicle)
        {
            case Sedan _:
                Console.WriteLine("This vehicle is a sedan");
                break;

            case Semi<TLoad> semi:
                Console.WriteLine($"This vehicle is a semi carrying {semi.Load.Name}");
                break;

            default:
                Console.WriteLine("We don't know what kind of vehicle this is.");
                break;
        }
    }
}

=== 8< ======================= previous version ======================= 8< ===

I guess, you have to use AbstractLoad as inner type, since Semi is not directly the same type as Semi:

 class Program
    {
        static void Main(string[] args)
        {
            var vehicle = new Semi<AbstractLoad> { Load = new Oil() };

            DescribeVehicle(vehicle);
        }

        static void DescribeVehicle(Vehicle vehicle)
        {
            switch (vehicle)
            {
                case Sedan _:
                    Console.WriteLine("This vehicle is a sedan");
                    break;

                case Semi<AbstractLoad> semi:
                    Console.WriteLine($"This vehicle is a semi carrying {semi.Load.Name}");
                    break;

                default:
                    Console.WriteLine("We don't know what kind of vehicle this is.");
                    break;
            }
        }
    }

This version works.

Solution 2. (suggested by LWChris)

Version with both methods:

https://dotnetfiddle.net/Widget/Preview?url=/Widget/3yVevr

class Program
{
    static void Main(string[] args)
    {
        var vehicle = new Semi<Oil> { Load = new Oil() };
        DescribeVehicle(vehicle);
        var sedan = new Sedan();
        DescribeVehicle(sedan);
    }

    static void DescribeVehicle(Vehicle vehicle)
    {
        switch (vehicle)
        {
            case Sedan _:
                Console.WriteLine("This vehicle is a sedan");
                break;

            case Semi<AbstractLoad> semi:
                Console.WriteLine($"This vehicle is a semi carrying {semi.Load.Name}");
                break;

            default:
                Console.WriteLine("We don't know what kind of vehicle this is.");
                break;
        }
    }

    static void DescribeVehicle<TLoad>(Semi<TLoad> vehicle) where TLoad : AbstractLoad
    {
        switch (vehicle)
        {
            case Semi<TLoad> semi:
                Console.WriteLine($"This vehicle is a semi carrying {semi.Load.Name}");
                break;

            default:
                Console.WriteLine("We don't know what kind of vehicle this is.");
                break;
        }
    }
}
Related