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

这只是我在这里侵犯的普通不良代码气味,还是可以消除上述方法的歧义,以便编译器始终正确获取调用代码?

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

4

4 回答 4

8

重命名其中一种方法。您会注意到List<T>有一个 Add 和 AddRange 方法;遵循这种模式。对一个项目做某事和对一系列项目做某事在逻辑上是不同的任务,所以让这些方法有不同的名字。

于 2010-10-08T14:14:50.020 回答
5

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

第一个问题是约束不是方法签名的一部分,并且不会被考虑用于重载解决方案。

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

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

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

首先,请注意,在上面的示例中,我们选择忽略返回参数。其次,注意我们Frobble()Customers集合上调用了一些方法。现在你能告诉我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);

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

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

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

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

于 2010-10-08T14:31:28.193 回答
2

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

于 2010-10-08T14:15:27.273 回答
1

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

话虽如此,您可以使用 IEnumerable 重载的约束...也许将您的方法更改为具有两个<X, T>具有类似 " where X : IEnumerable<T>" 约束的通用类型?

ETA 以下代码示例:

  void T[] GetOrAdd<X,T> (string cachekey, Func<X> fnGetItem) 
            where X : IEnumerable<T>
    { 
    }
于 2010-10-08T14:00:16.747 回答