I prevent a specific type using general restrictions

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

I would like to make the methods generalized and overloaded and limit the compiler from trying to bind to the non-enumeration method when the generic type is enumerated ...

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

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 a bad code smell that I am breaking here, or can the above methods be eliminated so that the compiler always gets the calling code?

Raise the vote for everyone who can give any answer, except for "rename one of the methods."

+6
generics c # restriction
source share
4 answers

Use only one method and detect the IEnumerable<T> case dynamically, rather than trying to accomplish the impossible with general constraints. It would be a “code smell” to deal with two different caching methods, depending on whether the object to be stored / retrieved is something enumerated or not. Also, just because it implements IEnumerable<T> does not mean that it is necessarily a collection.

+2
source share

Rename one of the methods. You will notice that List<T> has an Add and AddRange method; follow this pattern. Doing an element and doing something for a sequence of elements are logically different tasks, so methods have different names.

+8
source share

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

The first problem is that restrictions are not part of the method signature and will not be considered to allow overloading.

The second problem you have to overcome is that the compiler chooses the best match for the available signatures - which when working with generics usually means SomeMethod<T>(T) will be considered a better match than SomeMethod<T>( IEnumerable<T> ) . especially if you have parameters like T[] or List<T> .

But more fundamentally, you should think about whether working on a single value in comparison with a set 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 in which it can be argued that the semantic differences between individual objects and collections of objects do not make sense ... but in this case, why use two different methods in general? It is not clear that method overloading is the best way to express differences. Let's look at an example that gives confusion:

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

First, note that in the above example, we prefer to ignore the returned parameter. Secondly, note that we call some Frobble() method in the Customers collection. Now can you tell me which overload of GetOrAdd() will be called? Obviously, without knowing the type returned by Frobble() , this is not possible. Personally, I believe that code whose semantics cannot be easily understood from the syntax should be avoided whenever possible. If we choose the best names, this problem will be fixed:

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

Ultimately, there are only three options for eliminating the ambiguous methods in your example:

  • Change the name of one of the methods.
  • Pass an IEnumerable<T> wherever you call the second overload.
  • Change the signature of one of the methods so that the compiler can distinguish.

Option 1 is self-evident, so I will no longer talk about it.

Features 2 are also easy to understand:

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

Option 3 is harder. Let's see how we can achieve this.

The approach uses a Func<> delegate signature change, for example:

  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 horrible ... but unfortunately it will work.

An alternative way to change the signature (which is somewhat less scary) is to make the return value a 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); 

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

The last alternative that I will introduce is to add another common parameter to methods that identify the type of 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 you can use the hints to help the compiler choose the overload for us ... of course. But look at all the extra work that we have to do as a developer to get there (not to mention the introduction of ugliness and the possibility of mistakes). Is it really worth the effort? In particular, when is there a simple and reliable technique (naming methods differently) to help us?

+5
source share
Limitations

they don’t support an exception, which at first may seem disappointing, but consistent and makes sense (consider, for example, that interfaces do not determine which implementations cannot execute).

As the saying goes, you can get around the limitations of your IEnumerable overload ... maybe change your method to have two typical <X, T> types with a restriction like " where X : IEnumerable<T> "?

ETA is the following code example:

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

All Articles