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

发布于 2024-09-26 20:06:04 字数 786 浏览 0 评论 0原文

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

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

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

要 这只是我在这里侵犯的糟糕代码味道,或者是否可以消除上述方法的歧义,以便编译器始终获得正确的调用代码?

为任何能够给出“重命名其中一种方法”之外的答案的人投票。

I have an overload method - the first implementation always returns a single object, the second implementation always returns an enumeration.

I'd like to make the methods generic and overloaded, and restrict the compiler from attempting to bind to the non-enumeration method when the generic type is enumerable...

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

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

To be used with...

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

Is this just plain bad code-smell that I'm infringing on here, or is it possible to disambiguate the above methods so that the compiler will always get the calling code right?

Up votes for anyone who can produce any answer other than "rename one of the methods".

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

酷遇一生 2024-10-03 20:06:04

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

Rename one of the methods. You'll notice that List<T> has an Add and and AddRange method; follow that pattern. Doing something to an item and doing something to a sequence of items are logically different tasks, so make the methods have different names.

趁年轻赶紧闹 2024-10-03 20:06:04

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

第一个问题是 约束不是签名的一部分方法,不会被考虑进行重载解析。

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

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

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

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

This is a difficult use case to support because of how the C# compiler performs overload resolution and how it decides which method to bind to.

The first issue is that constraints are not part of the signature of a method and won't be considered for overload resolution.

The second problem you've got to overcome is that the compiler chooses the best match from the available signatures - which, when dealing with generics, generally means that SomeMethod<T>(T) will be considered a better match than SomeMethod<T>( IEnumerable<T> ) ... particularly when you've got parameters like T[] or List<T>.

But more fundamentally, you have to consider whether operating on a single value vs. a collection of values is really the same operation. If they are logically different, then you probably want to use different names just for clarity. Perhaps there are some use cases where you could argue that the semantic differences between single objects and collections of objects are not meaningful ... but in that case, why implement two different methods at all? It's unclear that method overloading is the best way to express the differences. Let's look at an example that lends to the confusion:

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

First, note that in the example above we are choosing to ignore the return parameter. Second, notice that we call some method Frobble() on the Customers collection. Now can you tell me which overload of GetOrAdd() will be called? Clearly without knowing the type that Frobble() returns it's not possible. Personally I believe that code whose semantics can't be readily inferred from the syntax should be avoided when possible. If we choose better names, this issue is alleviated:

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

Ultimately, there are only three options to disambiguate the methods in your example:

  1. Change the name of one of the methods.
  2. Cast to IEnumerable<T> wherever you call the second overload.
  3. Change the signature of one of the methods in a way that the compiler can differentiate.

Option 1 is self-evident, so I'll say no more about it.

Options 2 is also easy to understand:

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

Option 3 is more complicated. Let's look at ways we can be achieve it.

On approach is by changing the signature of the Func<> delegate, for instance:

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

Personally, I find this option terribly ugly, unintuitive, and confusing. Introducing an unused parameter is terrible ... but, sadly it will work.

An alternative way of changing the signature (which is somewhat less terrible) is to make the return value an out parameter:

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

But is this really better? It prevents us from using these methods as parameters of other method calls. It also makes the code less clear and less understandable, IMO.

A final alternative I'll present is to add another generic parameter to the methods which identifies the type of the return value:

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

So can use hints to help the compiler to choose an overload for us ... sure. But look at all of the extra work we have to do as the developer to get there (not to mention the introduced ugliness and opportunity for mistakes). Is it really worth the effort? Particularly when an easy and reliable technique (naming the methods differently) already exists to help us?

氛圍 2024-10-03 20:06:04

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

Use only one method and have it detect the IEnumerable<T> case dynamically rather than attempting the impossible via generic constraints. It would be "code smell" to have to deal with two different cache methods depending on if the object to store/retrieve is something enumerable or not. Also, just because it implements IEnumerable<T> does not mean it is necessarily a collection.

樱娆 2024-10-03 20:06:04

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

话虽这么说,您可以尝试使用 IEnumerable 重载的约束...也许可以更改您的方法以具有两个通用类型 ,并具有类似“where X”的约束: IEnumerable" ?

预计以下代码示例:

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

constraints don't support exclusion, which may seem frustrating at first, but is consistent and makes sense (consider, for example, that interfaces don't dictate what implementations can't do).

That being said, you could play around with the constraints of your IEnumerable overload...maybe change your method to have two generic typings <X, T> with a constraint like "where X : IEnumerable<T>" ?

ETA the following code sample:

  void T[] GetOrAdd<X,T> (string cachekey, Func<X> fnGetItem) 
            where X : IEnumerable<T>
    { 
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文