我使用通用限制来阻止特定类型

时间:2010-10-08 13:46:29

标签: c# generics restriction

我有一个重载方法 - 第一个实现总是返回一个对象,第二个实现总是返回一个枚举。

我想使方法泛型重载,并限制编译器在泛型类型可枚举时尝试绑定到非枚举方法...

class Cache
{
    T GetOrAdd<T> (string cachekey, Func<T> fnGetItem)
        where T : {is not IEnumerable}
    {
    }

    T[] GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem)
    {
    }
}

与...一起使用

{
    // The compile should choose the 1st overload
    var customer = Cache.GetOrAdd("FirstCustomer", () => context.Customers.First());

    // The compile should choose the 2nd overload
    var customers = Cache.GetOrAdd("AllCustomers", () => context.Customers.ToArray());
}

这只是我在这里侵犯的明显不好的代码味道,还是可以消除上述方法的歧义,以便编译器始终能够正确获取调用代码?

除了“重命名其中一种方法”之外,任何能够产生任何答案的人都可以投票。

4 个答案:

答案 0 :(得分:8)

重命名其中一种方法。您会注意到List<T>有一个Add和AddRange方法;遵循这种模式。对某个项目执行某些操作并对一系列项目执行某些操作是逻辑上不同的任务,因此请使这些方法具有不同的名称。

答案 1 :(得分:5)

这是一个难以支持的用例,因为C#编译器执行重载解析以及如何决定绑定哪个方法。

第一个问题是方法的constraints are not part of the signature,不会考虑重载解析。

您必须克服的第二个问题是编译器从可用签名中选择最佳匹配 - 在处理泛型时,通常意味着SomeMethod<T>(T)将被视为比{{1更好的匹配特别是当你有SomeMethod<T>( IEnumerable<T> )T[]等参数时。

但更重要的是,您必须考虑对单个值和值集合进行操作是否真的是相同的操作如果它们在逻辑上不同,那么为了清楚起见,您可能想要使用不同的名称。也许有一些用例可以证明单个对象和对象集合之间的语义差异没有意义......但在这种情况下,为什么要实现两种不同的方法呢?目前还不清楚方法重载是表达差异的最佳方式。让我们看一个导致混乱的例子:

List<T>

首先,请注意,在上面的示例中,我们选择忽略return参数。其次,请注意我们在Cache.GetOrAdd("abc", () => context.Customers.Frobble() ); 集合上调用了一些方法Frobble()。现在你能告诉我Customers的哪个超载会被调用吗?显然不知道GetOrAdd()返回的类型是不可能的。 就个人而言,我认为应该尽可能避免使用语法无法从语法中推断出语义的代码。如果我们选择更好的名称,这个问题就会得到缓解:

Frobble()

最终,只有三个选项可以消除示例中方法的歧义:

  1. 更改其中一种方法的名称。
  2. 只要您调用第二次重载,就转移到Cache.Add( "abc", () => context.Customers.Frobble() ); Cache.AddRange( "xyz", () => context.Customers.Frobble() );
  3. 以编译器可以区分的方式更改其中一个方法的签名。
  4. 选项1是不言而喻的,所以我不再赘述。

    选项2也很容易理解:

    IEnumerable<T>

    选项3更复杂。让我们看看我们可以实现的方式。

    方法是改变var customers = Cache.GetOrAdd("All", () => (IEnumerable<Customer>)context.Customers.ToArray()); 委托的签名,例如:

    Func<>

    就个人而言,我觉得这个选项非常丑陋,不直观,令人困惑。引入一个未使用的参数是可怕的......但是,遗憾的是它会起作用。

    更改签名的另一种方法(稍微不那么糟糕)是将返回值设为 T GetOrAdd<T> (string cachekey, Func<object,T> fnGetItem) T[] GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem) // now we can do: var customer = Cache.GetOrAdd("First", _ => context.Customers.First()); var customers = Cache.GetOrAdd("All", () => context.Customers.ToArray()); 参数:

    out

    但这真的更好吗?它阻止我们将这些方法用作其他方法调用的参数。它还使代码不那么清晰,也不太容易理解,IMO。

    我将提出的最后一个替代方法是在方法中添加另一个通用参数来标识返回值的类型:

    void GetOrAdd<T> (string cachekey, Func<object,T> fnGetItem, out T);
    void GetOrAdd<T> (string cachekey, Func<IEnumerable<T>> fnGetItem, out T[])
    
    // now we can write:
    Customer customer;
    Cache.GetOrAdd("First", _ => context.Customers.First(), out customer);
    
    Customer[] customers;
    var customers = Cache.GetOrAdd("All", 
                                   () => context.Customers.ToArray(), out customers);
    

    所以可以使用提示来帮助编译器为我们选择一个重载...当然。但是看看我们作为开发者必须做的所有额外工作(更不用说引入的丑陋和错误的机会)。真的值得努力吗?特别是当一种简单可靠的技术(命名方法不同)已经存在以帮助我们时?

答案 2 :(得分:2)

仅使用一种方法,让它动态检测IEnumerable<T>情况,而不是通过通用约束尝试不可能的情况。根据存储/检索的对象是否是可枚举的东西,必须处理两种不同的缓存方法将是“代码味道”。另外,仅仅因为它实现IEnumerable<T>并不意味着它必然是一个集合。

答案 3 :(得分:1)

约束不支持排除,这可能看起来令人沮丧,但是一致且有意义(例如,考虑接口不指示不能做什么的实现)。

话虽如此,您可以使用IEnumerable重载的约束...可能会改变您的方法,使其具有两个通用类型<X, T>,其约束条件为“where X : IEnumerable<T>”?

ETA以下代码示例:

  void T[] GetOrAdd<X,T> (string cachekey, Func<X> fnGetItem) 
            where X : IEnumerable<T>
    { 
    }