我能防止使用通用限制来阻止特定类型吗?

6

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

我想让这些方法变成通用的 并且 重载的,并且在泛型类型为可枚举时,限制编译器不尝试绑定到非枚举方法...

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

这只是一种常见的代码坏味道吗?还是有可能通过消除歧义让编译器始终正确地调用上述方法?

如果有其他答案而不是“重命名其中一个方法”,那么给予点赞。


我想知道将最不喜欢的编写为扩展方法是否可以完成任务。未经测试。 - Marc Gravell
3
更进一步增加复杂性,需要注意的是您可能不想排除string类型的项目,因为string实现了IEnumerableIEnumerable<char> - herzmeister
我刚注意到,除非我漏了什么,否则这些声明都无法编译,因为你同时说了 void 和返回类型。如果没有 void,你将试图对返回类型进行重载,这是不允许的,甚至不考虑一般性问题。 - AakashM
4个回答

8
重命名其中一个方法。您会注意到 List<T> 有一个 Add 和 AddRange 方法;按照这种模式操作。对于一项任务和一系列任务的操作是逻辑上不同的任务,因此请使这两个方法具有不同的名称。

1
看起来OP正在设计一个缓存接口。我自己已经写过一个,我认为缓存不应该关心要存储的对象是集合还是其他类型;那只是序列化过程的实现细节。在我看来,只需要一个方法就可以满足所有需求。 - James Dunne
@MojoFilter:你笑什么? - James Dunne
1
Mark:给那些能提供除“重命名其中一个方法”之外的任何答案的人点赞。Eric:重命名其中一个方法。 - MojoFilter

5
这是一个难以支持的使用案例,因为C#编译器如何执行重载决策和绑定方法。
第一个问题是约束不是方法签名的一部分,不会被考虑在重载决策中。
第二个问题是编译器从可用签名中选择最佳匹配,通常意味着在处理泛型时,SomeMethod<T>(T)将被认为比SomeMethod<T>( IEnumerable<T> )更匹配...特别是当你有像T[]List<T>这样的参数时。

但更基本的是,你必须考虑操作单个值与操作一组值是否真的是相同的操作。如果它们在逻辑上不同,那么为了清晰起见,您可能需要使用不同的名称。也许有一些用例可以认为单个对象和对象集合之间的语义差异并不重要...但在这种情况下,为什么要实现两种不同的方法呢?不清楚方法重载是否是表达差异的最佳方式。让我们看一个容易混淆的例子:

Cache.GetOrAdd("abc", () => context.Customers.Frobble() );

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

Cache.Add( "abc", () => context.Customers.Frobble() );
Cache.AddRange( "xyz", () => context.Customers.Frobble() );

最终,你的示例中只有三种消除歧义的选项:

  1. 更改其中一个方法的名称。
  2. 在调用第二个重载时将其转换为 IEnumerable<T>
  3. 更改其中一个方法的签名,以便编译器可以区分它们。

选项1是不言自明的,所以我就不再多说了。

选项2也很容易理解:

var customers = Cache.GetOrAdd("All", 
     () => (IEnumerable<Customer>)context.Customers.ToArray());

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

一种方法是通过改变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”参数。
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);

但这真的更好吗?它阻止我们将这些方法作为其他方法调用的参数使用。在我看来,它也使代码变得不太清晰和不易理解。
我将提出最后一个替代方案,即向方法添加另一个通用参数,用于标识返回值的类型:
T GetOrAdd<T> (string cachekey, Func<T> fnGetItem);
R[] GetOrAdd<T,R> (string cachekey, Func<IEnumerable<T>> fnGetItem);

// now we can do:
var customer = Cache.GetOrAdd("First", _ => context.Customers.First());
var customers = Cache.GetOrAdd<Customer,Customer>("All", () => context.Customers.ToArray());

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

2

只使用一种方法,并使其动态检测IEnumerable<T>情况,而不是通过泛型限制尝试不可能的事情。如果必须根据要存储/检索的对象是否可枚举而处理两种不同的缓存方法,那么这将是“代码异味”。此外,仅因为它实现了IEnumerable<T>并不意味着它一定是一个集合。


我认为你已经基本掌握了它。在反思所发布的答案后,我相信我尝试的是糟糕的小代码...如果调用代码应该对方法的不同工作方式毫不知情,那么方法签名中就不应该有区别...也就是说...应该有一个方法,正如你指出的那样,在内部进行检查。谢谢! - Mark
@Mark - 希望这有所帮助。只是想防止您解决错误的问题 :)。接受答案了吗? - James Dunne

1

约束不支持排除,这一点起初可能令人沮丧,但是它是一致的并且有意义(例如考虑到接口不规定实现不能做什么)。

话虽如此,您可以尝试调整IEnumerable重载的约束……也许更改您的方法,使其具有两个通用键入<X,T>,并添加类似于“where X:IEnumerable<T>” 的约束?

预计的代码示例如下:

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

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