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

Mar*_*ark 6 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)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

与...一起使用

{
    // 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());
}
Run Code Online (Sandbox Code Playgroud)

这只是一个简单的坏代码 - 我在这里侵犯了代码,或者是否可以消除上述方法的歧义,以便编译器始终可以获得正确的调用代码?

除了"重命名其中一种方法"之外,任何可以产生任何答案的人的投票.

Eri*_*ert 8

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

  • @Mojofilter:很明显,我不是为了赞成. (3认同)

LBu*_*kin 5

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

第一个问题是约束不是方法签名一部分,不会考虑重载解析.

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

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

Cache.GetOrAdd("abc", () => context.Customers.Frobble() );
Run Code Online (Sandbox Code Playgroud)

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

Cache.Add( "abc", () => context.Customers.Frobble() );
Cache.AddRange( "xyz", () => context.Customers.Frobble() );
Run Code Online (Sandbox Code Playgroud)

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

  1. 更改其中一个方法的名称.
  2. 转到IEnumerable<T>你调用第二个重载的地方.
  3. 以编译器可以区分的方式更改其中一个方法的签名.

选项1是不言而喻的,所以我不再说它了.

选项2也很容易理解:

var customers = Cache.GetOrAdd("All", 
     () => (IEnumerable<Customer>)context.Customers.ToArray());
Run Code Online (Sandbox Code Playgroud)

选项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());
Run Code Online (Sandbox Code Playgroud)

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

更改签名的另一种方法(稍微不那么糟糕)是将返回值作为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);
Run Code Online (Sandbox Code Playgroud)

但这真的更好吗?它阻止我们将这些方法用作其他方法调用的参数.它还使代码不那么清晰,也不太容易理解,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());
Run Code Online (Sandbox Code Playgroud)

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


Jam*_*nne 2

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