我使用通用限制来阻止特定类型
我有一个重载方法 – 第一个实现总是返回一个对象,第二个实现总是返回一个枚举。
我想使方法通用和重载,并限制编译器在generics类型可枚举时尝试绑定到非枚举方法…
class Cache { T GetOrAdd (string cachekey, Func fnGetItem) where T : {is not IEnumerable} { } T[] GetOrAdd (string cachekey, Func<IEnumerable> 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()); }
这只是我在这里侵犯的明显错误的代码味道,还是可以消除上述方法的歧义,以便编译器始终获得正确的调用代码?
除了“重命名其中一种方法”之外,任何能够产生任何答案的人的投票。
仅使用一种方法,让它动态检测IEnumerable
情况,而不是通过通用约束尝试不可能的情况。 根据存储/检索的对象是否是可枚举的东西,必须处理两种不同的缓存方法将是“代码味道”。 另外,仅仅因为它实现IEnumerable
并不意味着它必然是一个集合。
重命名其中一种方法。 你会注意到List
有一个Add和AddRange方法; 遵循这种模式。 对某个项目执行某些操作并对一系列项目执行某些操作是逻辑上不同的任务,因此请使这些方法具有不同的名称。
这是一个难以支持的用例,因为C#编译器如何执行重载解析以及它如何决定绑定哪个方法。
第一个问题是约束不是方法签名的一部分,不会考虑重载解决。
你必须克服的第二个问题是编译器从可用的签名中选择最佳匹配 – 在处理generics时,通常意味着SomeMethod
将被认为是比SomeMethod
更好的匹配SomeMethod
…特别是当你有T[]
或List
。
但更重要的是,您必须考虑对单个值和值集合进行操作是否真的是相同的操作 。 如果它们在逻辑上不同,那么为了清楚起见,您可能希望使用不同的名称。 也许有一些用例可以certificate单个对象和对象集合之间的语义差异没有意义……但在这种情况下,为什么要实现两种不同的方法呢? 目前还不清楚方法重载是表达差异的最佳方式。 让我们看一个导致混乱的例子:
Cache.GetOrAdd("abc", () => context.Customers.Frobble() );
首先,请注意,在上面的示例中,我们选择忽略return参数。 其次,请注意我们在Customers
集合上调用了一些方法Frobble()
。 现在你能告诉我将调用哪个GetOrAdd()
重载? 很明显,在不知道Frobble()
返回的类型的情况下,这是不可能的。 就个人而言,我认为应尽可能避免使用语法无法从语法中推断出语义的代码。 如果我们选择更好的名称,这个问题就会缓解:
Cache.Add( "abc", () => context.Customers.Frobble() ); Cache.AddRange( "xyz", () => context.Customers.Frobble() );
最终,只有三个选项可以消除示例中方法的歧义:
- 更改其中一个方法的名称。
- 无论你在哪里调用第二个重载,都转换为
IEnumerable
。 - 以编译器可以区分的方式更改其中一个方法的签名。
选项1是不言而喻的,所以我不再赘述。
选项2也很容易理解:
var customers = Cache.GetOrAdd("All", () => (IEnumerable)context.Customers.ToArray());
选项3更复杂。 让我们来看看我们可以实现它的方法。
方法是通过更改Func<>
委托的签名,例如:
T GetOrAdd (string cachekey, Func (string cachekey, Func> fnGetItem) // now we can do: var customer = Cache.GetOrAdd("First", _ => context.Customers.First()); var customers = Cache.GetOrAdd("All", () => context.Customers.ToArray());
就个人而言,我发现这个选项非常丑陋,不直观,令人困惑。 引入一个未使用的参数是可怕的……但是,遗憾的是它会起作用。
更改签名的另一种方法(稍微不那么糟糕)是将返回值设为out
参数:
void GetOrAdd (string cachekey, Func (string cachekey, Func> 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);
但这真的更好吗? 它阻止我们将这些方法用作其他方法调用的参数。 它还使代码不那么清晰,也不太容易理解,IMO。
我将提出的最后一个替代方法是在方法中添加另一个generics参数,用于标识返回值的类型:
T GetOrAdd (string cachekey, Func fnGetItem); R[] GetOrAdd (string cachekey, Func> fnGetItem); // now we can do: var customer = Cache.GetOrAdd("First", _ => context.Customers.First()); var customers = Cache.GetOrAdd("All", () => context.Customers.ToArray());
所以可以使用提示来帮助编译器为我们选择一个重载…当然。 但是看看我们作为开发人员到那里所做的所有额外工作(更不用说引入的丑陋和错误的机会)。 真的值得努力吗? 特别是当一种简单可靠的技术(命名方法不同)已经存在以帮助我们时?
约束不支持排除,这可能看起来令人沮丧,但是一致且有意义(例如,考虑接口不指示哪些实现不能做)。
话虽这么说,你可以玩你的IEnumerable重载的约束…也许改变你的方法有两个通用的类型
与约束,如“ where X : IEnumerable
”?
ETA以下代码示例:
void T[] GetOrAdd (string cachekey, Func fnGetItem) where X : IEnumerable { }