开发者

Conditional typing in generic method

开发者 https://www.devze.com 2022-12-20 11:28 出处:网络
Consider the following (heavily simplified) code: public T Function<T>() { if (typeof(T) == typeof(string)) {

Consider the following (heavily simplified) code:

public T Function<T>() {
    if (typeof(T) == typeof(string)) {
        return (T) (object) "hello";
    }
    ...
}

It's kind of absurd to first cast to object, then to T. But the compiler has no way of knowing that the previous test assured T is of type string.

What is the most elegant, idiomatic way of achieving this behavior in C# (which includes getting rid of the stupid typeof(T) == typeof(string), since T is string can't be used)?


Addendum: There is no return type variance in .net, so you can't make a function overload to type string (which, by the way, is just an example, but one reason why association end redefinition in polymorphism, e.g. UML, can't be done in c#). Obviously, the following would be great, but it doesn't work:

public T Function<T>() {
    ...
}

public string Function<string>() {
    return "hello";
}

Concrete Example 1: Because there's been several attacks to the fact that a generic function that tests for specific types isn't generic, I'll try to provide a more complete example. Consider the Type-Square design pattern. Here follows a snippet:

public class Entity {
  Dictionary<PropertyType, object> properties;

  public T GetTypedProperty<T>(PropertyType p) {
    var val = properties[p];

    if (typeof(T) == typeof(string) {
      (T) (object) p.ToString(this);  // magic going here
    }

    return (T) TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(val);
  }
}

Concrete Example 2: Consider the Interpreter design pattern:

public class Expression {
  public virtual object Execute() { }
}

public class StringExpression: Expression {
  public override string Execute() { }    // Error! Type variance not allowed...
}

Now let's use generics in Execute to allow the caller to force a return type:

public class Expression {
  public virtual T Execute<T>() { 
    if(typeof(T) == typeof(string)) {  // what happens when I want a string result from a non-string expression?
       return (T) (object) do_some_magic_and_return_a_string();
    } else if(typeof(T) == typeof(bool)) { // what about bools? any number != 0 should be True. Non-empty lists should be True. Not null should be True
       return (T) (object) do_some_magic_and_return_a_bool();
    }
  }
}

public class StringExpression: Expressiong {
  public override T Execute<T>() where T: string {  开发者_开发技巧 
    return (T) string_result;
  }
}


If you're making these types of checks in a generic method, I'd rethink your design. The method is obviously not truly generic - if it were, you wouldn't need specific type checking...

Situations like this typically can be handled more cleanly by a redesign. One alternative is often to provide an overload of the appropriate type. Other design alternatives which avoid the type-specific behavior exist, as well, such as Richard Berg's suggestion of passing in a delegate.


using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleExamples
{
    /// <summary>
    /// Compiled but not run.  Copypasta at your own risk!
    /// </summary>
    public class Tester
    {
        public static void Main(string[] args)
        {
            // Contrived example #1: pushing type-specific functionality up the call stack
            var strResult = Example1.Calculate<string>("hello", s => "Could not calculate " + s);
            var intResult = Example1.Calculate<int>(1234, i => -1);

            // Contrived example #2: overriding default behavior with an alternative that's optimized for a certain type
            var list1 = new List<int> { 1, 2, 3 };
            var list2 = new int[] { 4, 5, 6 };
            Example2<int>.DoSomething(list1, list2);

            var list1H = new HashSet<int> { 1, 2, 3 };
            Example2<int>.DoSomething<HashSet<int>>(list1H, list2, (l1, l2) => l1.UnionWith(l2));
        }
    }

    public static class Example1
    {
        public static TParam Calculate<TParam>(TParam param, Func<TParam, TParam> errorMessage)            
        {
            bool success;
            var result = CalculateInternal<TParam>(param, out success);
            if (success)
                return result;
            else
                return errorMessage(param);
        }

        private static TParam CalculateInternal<TParam>(TParam param, out bool success)
        {
            throw new NotImplementedException();
        }
    }

    public static class Example2<T>
    {
        public static void DoSomething(ICollection<T> list1, IEnumerable<T> list2)
        {
            Action<ICollection<T>, IEnumerable<T>> genericUnion = (l1, l2) =>
            {
                foreach (var item in l2)
                {
                    l1.Add(item);
                }
                l1 = l1.Distinct().ToList();
            };
            DoSomething<ICollection<T>>(list1, list2, genericUnion);
        }

        public static void DoSomething<TList>(TList list1, IEnumerable<T> list2, Action<TList, IEnumerable<T>> specializedUnion)
            where TList : ICollection<T>
        {
            /* stuff happens */

            specializedUnion(list1, list2);

            /* other stuff happens */            
        }
    }
}

/// I confess I don't completely understand what your code was trying to do, here's my best shot
namespace TypeSquarePattern
{
    public enum Property
    {
        A,
        B,
        C,
    }

    public class Entity
    {
        Dictionary<Property, object> properties;
        Dictionary<Property, Type> propertyTypes;

        public T GetTypedProperty<T>(Property p) 
        {
            var val = properties[p];
            var type = propertyTypes[p];

            // invoke the cast operator [including user defined casts] between whatever val was stored as, and the appropriate type as 
            // determined by the domain model [represented here as a simple Dictionary; actual implementation is probably more complex]
            val = Convert.ChangeType(val, type);  

            // now create a strongly-typed object that matches what the caller wanted
            return (T)val;
        }
    }
}

/// Solving this one is a straightforward application of the deferred-execution patterns I demonstrated earlier
namespace InterpreterPattern
{
    public class Expression<TResult>
    {
        protected TResult _value;             
        private Func<TResult, bool> _tester;
        private TResult _fallback;

        protected Expression(Func<TResult, bool> tester, TResult fallback)
        {
            _tester = tester;
            _fallback = fallback;
        }

        public TResult Execute()
        {
            if (_tester(_value))
                return _value;
            else
                return _fallback;
        }
    }

    public class StringExpression : Expression<string>
    {
        public StringExpression()
            : base(s => string.IsNullOrEmpty(s), "something else")
        { }
    }

    public class Tuple3Expression<T> : Expression<IList<T>>
    {
        public Tuple3Expression()
            : base(t => t != null && t.Count == 3, new List<T> { default(T), default(T), default(T) })
        { }
    }
}


Can you use as here?

T s = "hello" as T;
if(s != null)
    return s;


I can't think of an "elegant" way to do this. As you say, the compiler can't know that the conditional has ensured that the type of T is string. As a result, it has to assume that, since there's no generalized way to convert from string to T, it's an error. object to T might succeed, so the compiler allows it.

I'm not sure I'd want an elegant way to express this. Although I can see where it'd be necessary to do explicit type checks like this in some situations, I think I'd want it to be cumbersome because it really is a bit of a hack. And I'd want it to stick out: "Hey! I'm doing something weird here!"


Ok, I took a run at it from several different angles and came up short. I would have to conclude that if your current implementation gets the job done you should take the win and move on. Short of some arcane emissions what you got is what you get.

But the compiler has no way of knowing that the previous test assured T is of type string.

Umm.... If I am not mistaken, generics is just code gen. The compiler generates a matching method for each distinct type found in the calling methods. So the compiler does know the type argument for the overload being called. Again; If I am not mistaken.

But overall, i think you are misusing the generic in this case, from what I can see, and as others have stated, there are more appropriate solutions..... which are unnamable unless you post code that completely specifies your requirements.

just my 2 pesos...

0

精彩评论

暂无评论...
验证码 换一张
取 消