Generic method multiple (OR) type constraint

C#asp.net Mvc-3Types

C# Problem Overview


Reading this, I learned it was possible to allow a method to accept parameters of multiple types by making it a generic method. In the example, the following code is used with a type constraint to ensure "U" is an IEnumerable<T>.

public T DoSomething<U, T>(U arg) where U : IEnumerable<T>
{
    return arg.First();
}

I found some more code which allowed adding multiple type constraints, such as:

public void test<T>(string a, T arg) where T: ParentClass, ChildClass 
{
    //do something
}

However, this code appears to enforce that arg must be both a type of ParentClass and ChildClass. What I want to do is say that arg could be a type of ParentClass or ChildClass in the following manner:

public void test<T>(string a, T arg) where T: string OR Exception
{
//do something
}

Your help is appreciated as always!

C# Solutions


Solution 1 - C#

That is not possible. You can, however, define overloads for specific types:

public void test(string a, string arg);
public void test(string a, Exception arg);

If those are part of a generic class, they will be preferred over the generic version of the method.

Solution 2 - C#

Botz answer is 100% correct, here's a short explanation:

When you are writing a method (generic or not) and declaring the types of the parameters that the method takes you are defining a contract:

> If you give me an object that knows how to do the set of things that > Type T knows how to do I can deliver either 'a': a return value of the > type I declare, or 'b': some sort of behavior that uses that type.

If you try and give it more than one type at a time (by having an or) or try to get it to return a value that might be more than one type that contract gets fuzzy:

> If you give me an object that knows how to jump rope or knows how to calculate pi > to the 15th digit I'll return either an object that can go fishing or maybe mix > concrete.

The problem is that when you get into the method you have no idea if they've given you an IJumpRope or a PiFactory. Furthermore, when you go ahead and use the method (assuming that you've gotten it to magically compile) you're not really sure if you have a Fisher or an AbstractConcreteMixer. Basically it makes the whole thing way more confusing.

The solution to your problem is one of two possiblities:

  1. Define more than one method that defines each possible transformation, behavior, or whatever. That's Botz's answer. In the programming world this is referred to as Overloading the method.

  2. Define a base class or interface that knows how to do all the things that you need for the method and have one method take just that type. This may involve wrapping up a string and Exception in a small class to define how you plan on mapping them to the implementation, but then everything is super clear and easy to read. I could come, four years from now and read your code and easily understand what's going on.

Which you choose depends on how complicated choice 1 and 2 would be and how extensible it needs to be.

So for your specific situation I'm going to imagine you're just pulling out a message or something from the exception:

public interface IHasMessage
{
    string GetMessage();
}

public void test(string a, IHasMessage arg)
{
    //Use message
}

Now all you need are methods that transform a string and an Exception to an IHasMessage. Very easy.

Solution 3 - C#

If ChildClass means it is derived from ParentClass, you may just write the following to accept both ParentClass and ChildClass;

public void test<T>(string a, T arg) where T: ParentClass 
{
    //do something
}

On the otherhand, if you want to use two different types with no inheritance relation between them, you should consider the types implementing the same interface;

public interface ICommonInterface
{
    string SomeCommonProperty { get; set; }
}

public class AA : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;set;
    }
}

public class BB : ICommonInterface
{
    public string SomeCommonProperty
    {
        get;
        set;
    }
}

then you can write your generic function as;

public void Test<T>(string a, T arg) where T : ICommonInterface
{
    //do something
}

Solution 4 - C#

As old as this question is I still get random upvotes on my explanation above. The explanation still stands perfectly fine as it is, but I'm going to answer a second time with a type that's served me well as a substitute for union types (the strongly-typed answer to the question that's not directly supported by C# as is).

using System;
using System.Diagnostics;

namespace Union {
    [DebuggerDisplay("{currType}: {ToString()}")]
    public struct Either<TP, TA> {
        enum CurrType {
            Neither = 0,
            Primary,
            Alternate,
        }
        private readonly CurrType currType;
        private readonly TP primary;
        private readonly TA alternate;

        public bool IsNeither => currType == CurrType.Neither;
        public bool IsPrimary => currType == CurrType.Primary;
        public bool IsAlternate => currType == CurrType.Alternate;

        public static implicit operator Either<TP, TA>(TP val) => new Either<TP, TA>(val);

        public static implicit operator Either<TP, TA>(TA val) => new Either<TP, TA>(val);

        public static implicit operator TP(Either<TP, TA> @this) => @this.Primary;

        public static implicit operator TA(Either<TP, TA> @this) => @this.Alternate;

        public override string ToString() {
            string description = IsNeither ? "" :
                $": {(IsPrimary ? typeof(TP).Name : typeof(TA).Name)}";
            return $"{currType.ToString("")}{description}";
        }

        public Either(TP val) {
            currType = CurrType.Primary;
            primary = val;
            alternate = default(TA);
        }

        public Either(TA val) {
            currType = CurrType.Alternate;
            alternate = val;
            primary = default(TP);
        }

        public TP Primary {
            get {
                Validate(CurrType.Primary);
                return primary;
            }
        }

        public TA Alternate {
            get {
                Validate(CurrType.Alternate);
                return alternate;
            }
        }

        private void Validate(CurrType desiredType) {
            if (desiredType != currType) {
                throw new InvalidOperationException($"Attempting to get {desiredType} when {currType} is set");
            }
        }
    }
}

The above class represents a type that can be either TP or TA. You can use it as such (the types refer back to my original answer):

// ...
public static Either<FishingBot, ConcreteMixer> DemoFunc(Either<JumpRope, PiCalculator> arg) {
  if (arg.IsPrimary) {
    return new FishingBot(arg.Primary);
  }
  return new ConcreteMixer(arg.Secondary);
}

// elsewhere:

var fishBotOrConcreteMixer = DemoFunc(new JumpRope());
var fishBotOrConcreteMixer = DemoFunc(new PiCalculator());

Important Notes:

  • You'll get runtime errors if you don't check IsPrimary first.
  • You can check any of IsNeither IsPrimary or IsAlternate.
  • You can access the value through Primary and Alternate
  • There are implicit converters between TP/TA and Either to allow you to pass either the values or an Either anywhere where one is expected. If you do pass an Either where a TA or TP is expected, but the Either contains the wrong type of value you'll get a runtime error.

I typically use this where I want a method to return either a result or an error. It really cleans up that style code. I also very occasionally (rarely) use this as a replacement for method overloads. Realistically this is a very poor substitute for such an overload.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMansfieldView Question on Stackoverflow
Solution 1 - C#Botz3000View Answer on Stackoverflow
Solution 2 - C#Chris PfohlView Answer on Stackoverflow
Solution 3 - C#daryalView Answer on Stackoverflow
Solution 4 - C#Chris PfohlView Answer on Stackoverflow