-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate new convenience APIs into extension methods #20684
Comments
@terrajobst @stephentoub @weshaggard @danmosemsft @KrzysztofCwalina we should discuss it on Tuesday the latest ... @jkotas might want to join us if we expect difference in opinions during the discussion ;-) |
cc @ianhays @safern @AlexGhiondea @joperezr for Collections and Runtime areas which are potentially impacted the most in CoreFX. |
Remove -- https://github.com/dotnet/corefx/issues/15622 |
It is not a high cost for every API. It becomes high cost once the per-API cost gets multiplied by non-trivial number of APIs. Death by thousand paper cuts sort of problem. Also, this is not a black and white. Some new additions are fine - in particular the ones that make a significant dent in the usability of the type (e.g. new APIS on ArraySegment), but there is balance to strike. |
Here's a proposal (for common generic classes):
|
TryAdd was convenience but also to avoid two lookups when one would suffice, in a commonly-used pattern. The latter part can't be done externally to the type. |
Some apis get a benefit from being a member e.g. Others just use the public interface so shouldn't loose much from being an extension
Slightly tangential, but related does |
Thanks @stephentoub @benaadams for pointing out that |
Another reason for using extensions on generic types is it allows for specialisation of the methods based on type (e.g. |
We discussed this. Conclusion:
|
@ianhays can you please list the APIs we added in 2.0 on any collection? @weshaggard can help if needed. |
Is a principle established for API's that provide perf benefits, like TryAdd? I assume those can continue to go on the real types so long as they don't excessively bloat the code nor have a perf impact on existing API. |
It will be case by case decisions, but high-level principles are:
|
Huh, I just noticed that |
Sure thing @karelz. Here are all the APIs that are exclusive to netcoreapp2.0 added in System.Collections: namespace System.Collections
{
public sealed partial class BitArray : System.Collections.ICollection, System.Collections.IEnumerable, System.ICloneable
{
public System.Collections.BitArray RightShift(int count) { throw null; }
public System.Collections.BitArray LeftShift(int count) { throw null; }
}
}
namespace System.Collections.Generic
{
public static class CollectionExtensions
{
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) { throw null; }
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue) { throw null; }
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key) { throw null; }
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue) { throw null; }
}
public partial class Dictionary<TKey, TValue> : System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey, TValue>>, System.Collections.Generic.IDictionary<TKey, TValue>, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey, TValue>>, System.Collections.Generic.IReadOnlyCollection<System.Collections.Generic.KeyValuePair<TKey, TValue>>, System.Collections.Generic.IReadOnlyDictionary<TKey, TValue>, System.Collections.ICollection, System.Collections.IDictionary, System.Collections.IEnumerable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable
{
public Dictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey, TValue>> collection) { }
public Dictionary(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey, TValue>> collection, System.Collections.Generic.IEqualityComparer<TKey> comparer) { }
public bool TryAdd(TKey key, TValue value) { throw null; }
public TValue GetValueOrDefault(TKey key) { throw null; }
public TValue GetValueOrDefault(TKey key, TValue defaultValue) { throw null; }
}
public partial class HashSet<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.ISet<T>, System.Collections.IEnumerable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable
{
public HashSet(int capacity) { }
public HashSet(int capacity, System.Collections.Generic.IEqualityComparer<T> comparer) { }
public bool TryGetValue(T equalValue, out T actualValue) { throw null; }
}
public partial class Queue<T> : System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.ICollection, System.Collections.IEnumerable
{
public bool TryDequeue(out T result) { throw null; }
public bool TryPeek(out T result) { throw null; }
}
public partial class SortedSet<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.ISet<T>, System.Collections.ICollection, System.Collections.IEnumerable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable
{
public bool TryGetValue(T equalValue, out T actualValue) { throw null; }
}
public partial class Stack<T> : System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.ICollection, System.Collections.IEnumerable
{
public bool TryPeek(out T result) { throw null; }
public bool TryPop(out T result) { throw null; }
}
} To get this list i diffed our current System.Collections ref file with the oldest one on Github, then removed API's that were present in both or that were present in the recent ref file but a part of the last netfx release (i.e. netstandard2.0 APIs). |
@karelz do you think this is going to make it into 2.0.0 or should we probably move it to 2.1.0/future? |
@safern moving it doesn't make sense. Either we do it, or we close it. |
So with the list that @ianhays provided is it ready to be marked as api-ready-for-review so that it can be reviewed next week? |
Yes it is. I just did it (got distracted from doing that after my previous reply, sorry) |
No problem. Just wanted to make sure that it was good to go so that it is reviewed next Tuesday. Thanks! |
Yep, thanks for chasing it down, appreciate it! |
We've finalized our set of principles to this list:
For the APIs @safern posted earlier this means:
|
@safern please make sure we have appropriate issues for any work |
@terrajobst could you consider an addition to the list? DO prefer extension methods to instance methods on generic types if they gain no extra benefit from having access to non-public members. This is because the extension methods allow additional specialisation by type whereas generic instance methods allow no further specialization (from dotnet/corefxlab#1314 (comment)) As shown by class Program
{
static void Main(string[] args)
{
var value = new MyType<byte>();
value.Output();
}
}
public class MyType<T>
{
public void Output()
{
Console.WriteLine("Generic instance method called");
}
}
public static class MyTypeExtensions
{
public static void Output(this MyType<byte> myType)
{
Console.WriteLine("MyType<byte> Extension method called");
}
} Outputs
|
For value types, instance methods can specialize implementation via guarded type checks that'll result in dead branches being trimmed by the JIT. But for reference types it's true. And extensions let you add methods that only apply to certain instantiations, e.g. TaskExtensions.Unwrap, which can't be done with instance methods... and that may have been your main point (though at that point, it's not really a choice, since there's no way to do it with instance methods, unless you want to throw from every other usage). |
Only for ahead of time decided types. If a library author creates their own struct type that can't be added to the type check chain in the instance method and will always take the general object path. Obviously if there is a gain from access to internals (like the dictionary However, if its a method that is just using the public api and being an instance method gains no measurable benefit over an extension method; then it should be implemented as an extension method over the generic type; which then allows future specialisations for other types and future types to be written. |
Sure, in general, but this thread is about types in the BCL... types like Dictionary aren't going to provide methods that operate on BensCoolType, even as an extension method. 😄 |
You say that but... you could do public static GetValueOrDefault(this Dictionary<K,V> dict, K key)
{
if (dict.TryGet(out var value)
{
return value;
}
return default(V)
} Then the CoolType library could add public static GetValueOrDefault(this Dictionary<K,BensCoolType> dict, K key)
{
if (dict.TryGet(out var value)
{
return value;
}
return BensCoolType.DefaultValue;
} 😉 |
Or maybe you hate public static GetValueOrDefault(this Dictionary<K,string> dict, K key)
{
if (dict.TryGet(out var value)
{
return value;
}
return string.Empty;
} Rather than writing |
I think what you're really pointing out is that the C# compiler only considers extension methods if it can't find a matching instance method, and thus defining an instance method prevents others from providing their own replacement extensions with a more specialized signature. |
Yes exactly |
I see @benaadams's point, however, I am a little bit hesitant to optimize for method overloading in the compiler. IMO it creates quite a lot of complexity people need to be aware of ... (imagine you call GetValueOrDefault and the library you depend on overloaded it, but you don't know it and all you look at is public docs on MSDN and you are puzzled why it returns something else ...) If a type wants specialized extension method overload, I would suggest to the author to create a brand new extension method ... |
@terrajobst is it worth saving the rules in some API design doc in the repo? |
@karelz I think that is a good idea as for the future we can have that in a doc instead of looking for this specific issue/comment.
Taking care of that. |
My examples might have been poor; though the counter argument equally applies to virtual class methods; interfaces, operator overloading etc. The extension methods allow for a kind of inheritance of the generic type when it is an Perhaps a real example would have been better; If these were written as a single instance method with Anything that's a "delete a single method" I see as a better option than search and replace over your code base. Also extension method changes are easier to shim back onto desktop than instance method changes; as desktop moves at an even slower cadence than corefx (which is relatively quick) |
@benaadams your new example falls under [2] in @terrajobst's list. I agree with that one. |
I've opened issues: To track the work needed for this issue. So I will go ahead and close this issue. |
No. 😄 |
@terrajobst I have updated your approval post https://github.com/dotnet/corefx/issues/17275#issuecomment-291636863 with links to tracking issues (easier to check all are covered) and I added explanation that by |
Context: dotnet/android#723 [`corefx` removed ``Dictionary`2.GetValueOrDefault()``][cfx17275] in favor of using extension methods of the same name within `System.Collections.Generic.CollectionExtensions`, and mono/2017-06 is using the updated corefx sources. [cfx17275]: https://github.com/dotnet/corefx/issues/17275#issuecomment-291636863 @marek-safar [deems this as not a breaking change][1]: [1]: dotnet/android#631 (comment) > It breaks at binary compatibility level but not sure we made such strong promise. Update `reference/mscorlib.xml` to be consistent with mono/2017-06. Additionally, synchronize `reference/Mono.Security.xml` with mono/2017-06. We don't consider `Mono.Security.dll` to be part of our stable API.
Highly used generic types (mostly Collections) pay high cost for every API (method) we add to it.
We should revisit the decisions of adding new APIs (esp. convenience APIs) directly on the type and consider moving them into extension methods instead (in future and potentially revisit those we added during 2.0 and did not publish yet).
Context: see @jkotas's comment and guidance here: dotnet/coreclr#10203 (comment)
The text was updated successfully, but these errors were encountered: