泛型方法中的条件类型

14

考虑以下(大大简化的)代码:

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

在先将类型转换为object,然后转换为T的过程中,似乎有点荒谬。但是编译器并不知道之前的测试已经保证了T是字符串类型。在C#中,最优雅和惯用的方法是什么(包括摆脱愚蠢的typeof(T) == typeof(string),因为不能使用T is string)?
附:.net没有返回类型差异,因此无法创建以字符串类型重载的函数(例如,这只是一个例子,但这也是多态性中关联端重新定义(例如UML)不能在C#中完成的原因之一)。显然,下面的方法很好,但它不起作用。
public T Function<T>() {
    ...
}

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

具体例子1:由于有人曾经质疑测试特定类型的通用函数不是通用的,因此我将尝试提供一个更完整的示例。考虑Type-Square设计模式,以下是代码片段:

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);
  }
}

具体例子2:考虑解释器设计模式:

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

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

现在让我们在Execute中使用泛型,以允许调用者强制返回类型:
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;
  }
}

我很好奇...你能解释一下你将返回什么类型的值吗?例如,它们来自哪里,函数是否有任何隐含或显式的限制?我们是否应该假设只期望内在类型? - Sky Sanders
正如我在下面的评论中指出的那样,有些情况下调用者完全期望不同类型的函数具有不同的语义。例如,一些语言支持整数和字符串的+运算符;然而,对于整数,它作为求和运算符,而对于字符串,它则作为连接器。 - Hugo Sereno Ferreira
如果您对我需要这个模式感兴趣(请注意,我在不同的上下文中已经看到了这种情况多次),我有一个名为“Expression”的类的Evaluate<T>,它映射DSL的AST。底层语言是动态类型的,因此调用者不会事先知道确切的类型Evaluate<T>。但是,如果调用者强制指定一个特定类型,例如Evaluate<bool>,则该函数可以添加一些“语义糖果”,例如测试null、大于0的数字、非空列表等。无论发生什么,调用者都期望得到一个bool - Hugo Sereno Ferreira
当然,上面的例子容易受到设计攻击。有些人会建议我指出的逻辑应该在这些对象和bool之间的显式转换中。但想象一下,添加的“语义糖”取决于评估上下文。那么,在显式转换中存储逻辑将需要对象仅为此目的存储足够的信息。 - Hugo Sereno Ferreira
因此,这个问题的解决方案的上下文是内在类型和对象,并且您希望使用C# 3.0来强制实现多样化行为? - Sky Sanders
我根据Hugo提供的额外示例更新了我的答案。 - Richard Berg
5个回答

6
如果您在通用方法中进行这些类型的检查,我建议您重新考虑设计。该方法显然不是真正的“通用” - 如果是这样,您就不需要特定的类型检查...
这种情况通常可以通过重新设计更清晰地处理。一种替代方案通常是提供适当类型的重载。还存在其他避免特定于类型行为的设计替代方案,例如Richard Berg建议传递委托

很抱歉,我理解你的说法,但这种模式确实有合法的用途,当你想修改内部逻辑以应对特定类型出现时。而重载并不适用,因为函数的返回类型没有协变性。 - Hugo Sereno Ferreira
2
我非常笼统地同意,但这根本没有回答问题。 有时现实要求通用和特定功能在同一个堆栈帧中共存。(我想你可以复制/粘贴通用部分,但在我看来,这比这里的任何建议都更糟糕) 幸运的是,C#支持延迟执行,可以使用具有hacky但可行模式,就像我的回答中所示。 - Richard Berg
2
@Richard:我认为这是对问题的唯一有效回答。一个调用者怎么可能理解一个针对特定类型表现不同的通用方法的语义呢?如果OP解释了他真正想做什么,也许有人能够提供并解释更好的替代方案。 - Aaronaught
@Richard Berg:或许在这种情况下是这样,也或许不是。在他的例子中,你总是会从任何调用站点传递一个返回“Hello”的lambda表达式,这将违反DRY原则。如果没有看到更多“真实”的代码,很难知道最佳的设计选项是什么。我只是建议OP的设计可能不是合适的选择,因为它使得一个通用方法变成了非通用方法,如果你想要这个方法成为非通用方法,那么重载是更好的抽象。 - Reed Copsey
1
核心问题在于,相关的代码路径是否尽可能共享通用实现;如果是这样,如何实现。除非您知道如何解决C#的差异限制(例如使用委托),否则添加重载很可能会破坏DRY原则而不是改善它。 - Richard Berg
显示剩余11条评论

3
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) })
        { }
    }
}

Richard:+1 我喜欢这个设计,但在我看来,它回答的问题与 OP 不同。你(我认为是正确的)有意地从通用例程中提取类型特定信息,通过传递委托来处理来自调用方的信息。然而,在这里,通用方法并没有做任何“非通用”的事情——它对类型一无所知。但这是一个避免我提出的问题的好选择。 - Reed Copsey
1
说得好。 严格来说,我也建议Hugo重新设计他的界面,就像你一样。 然而:(1)很可能,我的解决方案与Hugo对于他的Function<T>有什么想法不太相距 [特别是如果他来自C++模板或函数式语言] (2)如果不是,它仍然比说“不要那样做”更有用。 - Richard Berg
有趣的解决方案,但是 DoSomething<TList> 返回 void,而我遇到的问题与返回类型的差异有关。除非我修改现有对象(在您的示例中,参数 TList list1),否则我无法将其推广到想要更改返回类型的情况。 - Hugo Sereno Ferreira
@Hugo - 实现返回类型的差异性与我最初示例中展示的差异并无不同。无论如何,我编辑了我的帖子,以展示您可能如何实现您添加到问题中的两种模式。如果有帮助,请告诉我。 - Richard Berg

1

这里可以使用 as 吗?

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

抱歉,但是当T为字符串时,"hello"应该是有条件地执行,而不是每次都测试是否为字符串。当然,这里的"hello"只是一个例子;我并不想走这么远只为了返回一些常量;-) - Hugo Sereno Ferreira
1
那么:if (typeof(T) == typeof(string)) return "hello" as T; - Anon.
不错的尝试!接近正确,但是 as 操作符只能在 T 是一个类的情况下使用。还是给你点赞。 - Hugo Sereno Ferreira
1
几乎听起来像是Hugo在煽动一场头脑风暴,实际上他已经有了解决方案,正在等待每个人都跳上失败之舟之前才揭示出来...哈哈。 - Sky Sanders
1
@Anon:如果您已经检查过T == typeof(string),那么不要使用as操作符进行转换,直接强制类型转换是安全和正确的。 - Oliver Friedrich
@BeowulfOF:虽然这样做可能是“安全的”,但问题是要求如何避免通过object进行双重转换。 - Anon.

1

我无法想到一种“优雅”的方式来解决这个问题。就像你所说的那样,编译器无法知道条件语句是否确保了T的类型是string。因此,它必须假定,由于没有一般化的方法可以从string转换为T,这是一个错误。object到T 可能会成功,因此编译器允许它。

我不确定我是否希望有一种优雅的方式来表达这个意思。虽然我可以看到在某些情况下需要明确进行此类类型检查,但我认为我希望它变得麻烦,因为这确实是一个小技巧。并且我希望它突出显示:“嘿!我在这里做一些奇怪的事情!”


0

好的,我从几个不同的角度尝试了一下,但都没有成功。我得出结论,如果你当前的实现可以完成工作,那么你应该接受这个结果并继续前进。除非有些神秘的问题,否则你已经得到了你想要的结果。

但编译器无法知道先前的测试确保T是字符串类型。嗯...如果我没记错,泛型只是代码生成。编译器为调用方法中找到的每个不同类型生成匹配的方法。因此,编译器确实知道正在调用的重载的类型参数。再次强调:如果我没有记错的话。但总的来说,我认为您在这种情况下误用了泛型,从我所看到的和其他人所说的来看,有更合适的解决方案......除非您发布完全指定您要求的代码,否则无法命名。仅代表我自己的2美分...

啊!问题来了:在C#中,泛型不仅仅是代码生成。它们与C ++中的模板完全不同。我会尝试提供一个更好的“好用法”的例子,但我怀疑这会使提出其他设计更容易(虽然人们可能会皱起眉头;-))。 - Hugo Sereno Ferreira
嗯,除非我错了,你是错的。;-)。这里有一个简洁明了的定义,我认为是正确的:http://www.codeproject.com/KB/cs/generics_explained.aspx,特别是“.NET运行时如何处理泛型”部分。我有什么遗漏吗? - Sky Sanders
好的...我明白你的意思了... .net泛型是在运行时生成的。我有点糊涂了。现在我完全转过来了...让我再从不同的角度来理解一下这个问题。 - Sky Sanders
你说的也没完全错。只是将泛型仅仅归结为代码生成有些过于简单化了。可以看看这个列表,它展示了泛型和模板之间的区别:http://msdn.microsoft.com/en-us/library/c6cyy67b(VS.80).aspx - Hugo Sereno Ferreira

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接