1

Can i constrain the type that is passed to my method in a way that catches this type of error when compiling instead of running?

My current code looks like this:

void Main()
{
    var dog = new Dog();
    SaveAnimal(dog);
}

void SaveAnimal(Animal animal) {
    var isAnimal = animal.GetType().UnderlyingSystemType == typeof(Animal);
    Debug.Assert(isAnimal, "You can not save dogs!");
}

class Animal {
    public int Legs { get; set; }
}

class Dog : Animal {
    public int TailLength { get; set; }
}
Espo
  • 41,399
  • 21
  • 132
  • 159
  • 1
    so basiaclly you want to allow all `Animal`s but keep the `Dog`s out ? – Mong Zhu Sep 05 '18 at 11:36
  • 2
    Doesn't that break LISP? – Camilo Terevinto Sep 05 '18 at 11:37
  • You can't do that as explained by @usr. You could perhaps call another method to filter out dogs before calling the SaveAnimal method? – Nick Sep 05 '18 at 11:45
  • 2
    If you have another animal, e.g. `Cat: Animal, ISaveable`, then `void SaveSaveable(ISaveable saveable)` can save it. Don't make dogs `ISaveable`, cats are so much better! – Sinatr Sep 05 '18 at 11:47
  • 1
    What if I implement `Cat` class as an *exact copy* of `Dog` class (I just copy and paste all the field/methods/properties of `Dog` class while renaming `Dog` int `Cat` when necessary)? Shall `SaveAnimal` *start working*? – Dmitry Bychenko Sep 05 '18 at 11:47
  • 6
    What you really need to ask yourself is: if a dog cannot do something *all* animals can, is a dog an animal at all? (Hint: it's not) – Camilo Terevinto Sep 05 '18 at 11:48

3 Answers3

6

No, there is no way in the language to statically catch this as a usage error.

You can assert this at runtime as you are doing it already. It's against the spirit of inheritance, though, because a fundamental assumption is that derived types must substitute for the base type (the Liskov substitution principle).

Maybe you can make the animal save itself by giving Animal a new method abstract void Save(). Then, each animal decides what to do. Dog can then throw a NotSupportedException.

usr
  • 168,620
  • 35
  • 240
  • 369
2

Yes, but only with a workaround using generics and interfaces.

What you would need to do is to declare 3 interfaces

public interface ISaveAbleBase { }
public interface ISaveAble : ISaveAbleBase{ }
public interface INotSaveAble : ISaveAbleBase { }

Now you need to give your Animal class a generic parameter and constraint it to be of type ISaveAbleBase.

class Animal<T> where T: ISaveAbleBase
{
    public int Legs { get; set; }
}

This way you can now specify in the derived classes wether they can or cannot be saved:

class Dog : Animal<INotSaveAble> 
{
    public int TailLength { get; set; }
}

Then you could make your method generic and constrain the type only to aminals that can be saved

void SaveAnimal<T>(T animal) where T: Animal<ISaveAble>

Now the result looks the following way:

void Main()
{
    var dog = new Dog();
    SaveAnimal(dog); // does not compile

    Animal<ISaveAble> generalAnimal = new Animal<ISaveAble>();
    SaveAnimal(generalAnimal); // compiles      
}

Disclaimer: this construct would also allow you to have a general Animal that cannot be saved:

Animal<INotSaveAble> generalAnimalNotSave = new Animal<INotSaveAble>();
SaveAnimal(generalAnimalNotSave); // does not compile

PS. This answer is inspired by this post

Mong Zhu
  • 23,309
  • 10
  • 44
  • 76
1

There is no standard way how to do this, but there is a simple (and stupid) workaround.

using System.Diagnostics;

namespace Test
{
  internal static class Program
  {
    private static void Main()
    {
      var dog = new Dog();
      SaveAnimal(dog);
    }

    private static void SaveAnimal(Animal animal)
    {
      var isAnimal = animal.GetType().UnderlyingSystemType == typeof(Animal);
      Debug.Assert(isAnimal, "You can not save dogs!");
    }

    private static void SaveAnimal(ICanNotSave animal)
    {
      Debug.Fail("Can not save");
    }
  }

  internal class Animal
  {
    public int Legs
    {
      get; set;
    }
  }

  internal interface ICanNotSave
  {
  }

  internal sealed class Dog : Animal, ICanNotSave
  {
    public int TailLength
    {
      get; set;
    }
  }
}

When you have two SaveAnimal method, where one of for Animal and other is for a interface, that is implemented on all descendants that can not be saved, the compiler report a CS0121 error.

The call is ambiguous between the following methods or properties: 'Program.SaveAnimal(Animal)' and 'Program.SaveAnimal(ICanNotSave)'

Remember it is still possible to use the SaveAnimal method when you use it like this: SaveAnimal((Animal)dog)

Julo
  • 1,102
  • 1
  • 11
  • 19
  • If there is a base class, then often (not always, but often) there is a collection of that type `List` to do something what all `Animal` can do. Then your hack won't catch the dog. – Sinatr Sep 05 '18 at 12:09
  • You are right. I forgot this one problem, but this can never be solved for compile time *(unless you make similar dirty work for the collection class)*. – Julo Sep 05 '18 at 12:21