[Discussion]: Extensions #8696
Replies: 451 comments 663 replies
-
I prefer using public extension Foo of int : IA, IB, IC, ...
{
...
} Otherwise it will be too confusing if you are extending an interface: public extension Foo : IA, IB, IC { } vs public extension Foo of IA : IB, IC { }
|
Beta Was this translation helpful? Give feedback.
-
I'm curious as to how the team weighs the relative benefits between "roles" and "extension implementation". It feels that without some additional effort in the runtime the two are somewhat incompatible with each other, so if those differences can't be reconciled which of the features might the team lean towards? Personally, I find extension implementation much more exciting than roles, but that's just my opinion. |
Beta Was this translation helpful? Give feedback.
-
@hez2010 public extension Foo for IA : IB, IC { } |
Beta Was this translation helpful? Give feedback.
-
Who gave you an early preview of my notes? They're up now, discussion at #5500. |
Beta Was this translation helpful? Give feedback.
-
Here's a scenario that will be great fun to try to accommodate in the design: interface IFoo { }
interface IBar { }
class Thing { }
public extension FooThing for Thing : IFoo { }
public extension BarThing for Thing : IBar { }
void Frob<T>(T t) where T : IFoo, IBar { }
Frob(new Thing()); On an unrelated bikeshedding note, what about using the existing reserved keywords |
Beta Was this translation helpful? Give feedback.
-
@sab39 Given, as you've mentioned, how similar these two concepts are. I too am looking for a good syntactic way to convey that similarity, with a clear way to do indicate in which way they differ. Thanks for the |
Beta Was this translation helpful? Give feedback.
-
I'm not sure if I should re-post my comments from the discussion here?
This is complicated, but doable using current constraints of the framework. An anonymous type can be generated: class <mangled>Thing_IFoo_IBar : IFoo, IBar
{
internal <mangled>Thing_IFoo_IBar(Thing thing) { this._thing = thing; }
readonly Thing _thing;
void IFoo.Foo() { ... } // these member(s) are copied from, or call into, FooThing
void IBar.Bar() { ... } // these member(s) are copied from, or call into, BarThing
}
Frob(new <mangled>Thing_IFoo_IBar(new Thing())); The same can be done for generic types, etc. Yes, it's complicated, but unlike roles, it's very possible. |
Beta Was this translation helpful? Give feedback.
-
This was just one example. It's not the main motivation. We discussed in the LDM that there were definitely plenty of scenarios where you'd still want adapters in a strongly typed way that would be sensible. |
Beta Was this translation helpful? Give feedback.
-
@TahirAhmadov That works, more or less, for the specific example I gave, but what if |
Beta Was this translation helpful? Give feedback.
-
If it's not the main motivation, surely it shouldn't be the one discussed in the OP, should it? |
Beta Was this translation helpful? Give feedback.
-
The OP is simply showing a demonstration. This is a broad topic and we need to spend a ton more time on it prior to even getting close to a place where we could write something up that was fully fleshed out and chock full of examples and whatnot. |
Beta Was this translation helpful? Give feedback.
-
The |
Beta Was this translation helpful? Give feedback.
-
Back with .NET Framework, I've often ran into situations where i wanted a The only thing I don't quite get is why we need two keywords here, |
Beta Was this translation helpful? Give feedback.
-
That's the thing, it would be very interesting to see an example which would demonstrate how |
Beta Was this translation helpful? Give feedback.
-
That's fine. It's something we're working on at this moment '-). The point was raised and was something we intend to get to and write more on. I def don't want us to get the impression that it's just for that. Thanks! |
Beta Was this translation helpful? Give feedback.
-
I really like this, but I am disappointed the "for" keyword got left behind for the "extension" group. |
Beta Was this translation helpful? Give feedback.
-
Not sure why this use case is not covered as it fails with CS9286: public static class Extensions {
//Define API:
extension<TEnum>(TEnum) where TEnum : unmanaged, Enum {
public static unsafe int BitSize {
get {
if (sizeof(TEnum) == 1) return 8;
if (sizeof(TEnum) == 2) return 16;
if (sizeof(TEnum) == 4) return 32;
if (sizeof(TEnum) == 8) return 64;
return 0;
}
}
}
//Consume API:
void Foo() {
var x = DayOfWeek.BitSize; // <- CS9286: 'DayOfWeek' does not contain a definition for 'BitSize' and no accessible extension member 'BitSize' for receiver of type 'DayOfWeek' could be found
}
} Yet, incidentally, the extension does show up in intellisense! I think this feature would be very useful particularly for |
Beta Was this translation helpful? Give feedback.
-
Why can't extension members have a backing field? internal static class ActionExtensions
{
extension<T>(Action<T>)
{
public static Action<T> OK=> _ => { };
public static Action<T> CS9282 { get; } = _ => { };
}
}
That's not a property? 🤔 |
Beta Was this translation helpful? Give feedback.
-
I'd be very greatful if the following (allowing // Small number of APIs that every random number provider has to implement
interface IRandomProvider
{
void NextBytes(Span<byte> bytes);
void Skip(UInt128 valuesToSkip);
// ...
}
// APIs which might be used on any instance, which have common logic built on top of those APIs
static class RandomProviderExtensions
{
// error CS9300: The 'ref' receiver parameter of an extension block must be a value type or a generic type constrained to struct.
// \/ The issue is here
extension(ref T provider) where T : IRandomProvider
{
public int NextInt32()
{
int value = 0;
provider.NextBytes(MemoryMarshal.AsBytes(new Span<int>(ref value)));
return value;
}
public int NextInt32(int minInclusive, int maxInclusive)
{
// Check arguments
var range = (uint)(maxInclusive - minInclusive + 1);
if (range == 0) return NextInt32();
if (range == 1) return minInclusive;
var mask = uint.RoundUpToPowerOf2(range) - 1;
uint result;
while (true)
{
result = (uint)NextInt32() & mask;
if (result < range) break;
}
return (int)result + minInclusive;
}
public int NextInt32(int maxInclusive) => NextInt32(0, maxInclusive);
// ... for all other data types, distribution types, etc.
}
}
// Struct provider
struct MyRandom
{
private int _state0, ...;
public void NextBytes() => ...;
// ...
}
// Class provider
class MyDifferentRandom
{
private UInt128 _state;
public void NextBytes() => ...;
// ...
}
// Example usage
static void Shuffle<T, TRandomProvider>(Span<T> values, ref TRandomProvider provider) where TRandomProvider : IRandomProvider
{
for (int i = values.Length - 1; i >= 1; i++)
{
var j = provider.NextInt32(i);
(values[i], values[j]) = (values[j], values[i]);
}
}
var mySpan = ...;
MyRandom r = ...; // or MyDifferentRandom
Shuffle(mySpan, ref r); // Will work with any random provider
// Example usage
MyRandom r1 = ...;
MyDiffererentRandom r2 = ...;
MyOtherDiffererentRandom r3 = ...;
// ...
// They all have implementations for NextInt32 that work immediately, along with any other common functionality I need.
// They don't need to be boxed to use it, nor do they need to repeat the same code over and over, nor do they need to be called
// in a less nice way, such as via a static method, nor do they need to all explicitly define the method themselves (lots of duplication), etc.
r1.NextByte(); // Imagine I used these for literally any use case for random number generators
r2.NextInt32();
r3.NextGaussian();
// ... Now I will list some other options that you could do something similar with today, but which are much less preferable to me for my use cases for the reasons I list. 2 seperate generic methods, one which takes by
Interface DIM:
Putting in a wrapper struct or ref struct, which we always take by ref or value (correspondingly), and then defining the extensions on that:
Making it a static method:
In summary, the main reason I think taking My understanding of why it's not allowed currently is because it may be confusing if I called a method on my class & the instance got replaced with another - this is an understandable concern, but I think not having a mechanism to allow defining instance members on generic types is more problematic (it's something I run into somewhat often, when working with mutable structs + interfaces + generics), and additionally, we have other language features which can be abused / used incorrectly / etc. (which for this feature, I'd argue that re-assigning Thanks for reading that & (hopefully) considering it :) |
Beta Was this translation helpful? Give feedback.
-
How about I was wondering if there are any plans for extension |
Beta Was this translation helpful? Give feedback.
-
It seems the C# Language Design Team are in dire need for a nice, intuitive syntax wich also supports all major cases while keeping binary compatibility with previous extension methods. So I went back and reread the comments from this discussion which have formed the state of the current language proposal as well as user-submitted ideas and concerns to address as many issues as possible. Here is my take of a possible syntax which could solve most goals: This is the very first example for Extension Members from the specs, rewritten in my proposed way: public extension<T> E for (T[] ts)
{
public bool M1(T t) => ts.Contains(t);
} Note that this isn't just a syntactical sugar over the current proposal, because this would generate a static class with a generic type parameter: [GeneratedCode]
public static class E<T>
{
public static bool M1(T[] ts, T t) => ts.Contains(t);
} instead of [GeneratedCode]
public static class E
{
public static bool M1<T>(T[] ts, T t) => ts.Contains(t);
} In this way we could preserve the arity of the generic arguments for the method, a major issue raised here: #8696 (comment) Furthermore, theoretically you would be able to omit the name of the extension and just rely on what the compiler can come up with: public extension<T> for (T[] ts)
{
public bool M1(T t) => ts.Contains(t);
} Which would be very similar to classless "extension groups" (a suggestion from Mads Torgersen's presentation). If needed, you could also alternatively write extension members individually in normal static class: public static class Enumerable
{
// Extension method with generic arity of 1
public extension<TSource> IEnumerable<TSource> Where(Func<TSource, bool> predicate)
for (IEnumerable<TSource> source)
{
...
}
// Extension method with generic arity of 2
public extension<TSource, TResult> IEnumerable<TResult> Select(Func<TSource, int, TResult> selector)
for (IEnumerable<TSource> source)
{
...
}
// Extension method with generic contsraint
public extension<TSource> IEnumerable<TSource> Average()
for (IEnumerable<TSource> source)
where TSource : INumberBase<TSource>
{
...
}
// Extension property
public extension<TSource> bool IsEmpty for (IReadOnlyCollection<TSource> source) => source.Count == 0;
// Extension operator
public extension<TSource> static IEnumerable<TSource> operator +(IEnumerable<TSource> first, IEnumerable<TSource> second)
for IEnumerable<TSource> // Note the lack of parentheses as they are not needed here
{
return source.Concat(other);
}
// Classic extension method
public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
// Non-extension member
public static IEnumerable<int> Range(int start, int count) { ... }
} In this way you can get binary compatibility with the old extension methods, but you would also lose those groupings, though. However, you can always add support for that syntax later for those who are interested in such "sugar". Here is the public extension<TSource> Enumerable for (IEnumerable<TSource> source)
{
public IEnumerable<TSource> Where(Func<TSource, bool> predicate)
{
...
}
public IEnumerable<TResult> Select<TResult>(Func<TSource, int, TResult> selector)
{
...
}
public IEnumerable<TResult> OfType<TResult>()
where TResult : TSource
{
...
}
} Note that this extension can't include the IEnumerable<Animal> animals = [dog, cat, goldfish];
IEnumerable<Mammal> mammals = animals.OfType<Mammal>(); because it would translate to IEnumerable<Mammal> mammals = Enumerable<Animal>.OfType<Mammal>(animals); And finally, here is an example for static only members: public extension MathExtensions for Math // Note the lack of parentheses as they are not needed here
{
public const double GoldenRatio = 1.61803398875;
public static BigInteger Pow(BigInteger value, int exponent)
{
return BigInteger.Pow(value, exponent);
}
public static T Pow<T>(T value, T exponent)
where T : IPowerFunctions<T>
{
return T.Pow(value, exponent);
}
} |
Beta Was this translation helpful? Give feedback.
-
Could we have 2-phase lookup by introducing discards for generic arguments that could use the old 1-phase lookup? extension(IEnumerable<_>)
{
// 1-phase lookup, for compat, generic argument in the method declaration
public static IEnumerable<T> M<T>(out T t) => ...;
}
extension<T>(IEnumerable<T>)
{
// 2-phase lookup, generic argument in the extension declaration
public static IEnumerable<T> M(out T t) => ...;
} Originally posted in dotnet/roslyn#78415 (comment). |
Beta Was this translation helpful? Give feedback.
-
I just ran into dotnet/roslyn#78753 when playing with the preview, and I find this behavior unintuitive. Intuitively, the extension method + extension property feels more like extension method + instance property than like instance method + instance property. Some part of this likely comes from the error message seeming to indicate that something wasn't found at all, when in fact the error is an ambiguous result. This limitation does also block a migration path for some code (which I, at least, see somewhat frequently). Previously, if you wanted a property-like thing as an extension, you'd write something like this (using the case I ran into as an example): public static bool IsByRefLike(this Type type) => ...
// called like
type.IsByRefLike() This has 2 downsides:
The ideal upgrade path for this is to keep (but hide) the existing extension method, while adding an extension property of the same name. This then allows the natural code to work, and gives access to pattern matching and such. Per @jcouv's response, this is not allowed under current rules, which is a shame. It would be very nice if there were some resolution. I think the natural one is to allow resolution here, but I would accept even a magic "don't consider this method during overload resolution" attribute (or some other similar cudgel). |
Beta Was this translation helpful? Give feedback.
-
I updated Visual Studio to Version 17.14.2 this week and since then, following code leads to a compile error: [GeneratedCode("xsd", "4.7.3081.0")]
[Serializable]
[DebuggerStepThrough]
[DesignerCategory("code")]
[XmlType(AnonymousType = true, Namespace = "http://www.ech.ch/xmlns/eCH-0020/3")]
[XmlRoot(Namespace = "http://www.ech.ch/xmlns/eCH-0020/3", IsNullable = false)]
public class extension
{
private XmlElement[] anyField;
/// <remarks/>
[XmlAnyElement]
public XmlElement[] Any
{
get
{
return anyField;
}
set
{
anyField = value;
}
}
}
[GeneratedCode("xsd", "4.7.3081.0")]
[Serializable]
[DebuggerStepThrough]
[DesignerCategory("code")]
[XmlType(Namespace = "http://www.ech.ch/xmlns/eCH-0020/3")]
public class eventChangeMatrimonialInheritanceArrangement
{
private extension extensionField;
public extension extension
{
get
{
return extensionField;
}
set
{
extensionField = value;
}
}
} The error is "error CS9281: Extension declarations may not have a name." at the Line "private extension extensionField;". I highly suspect, that a recent PR about "Extension Members" is the culprit. I was a bit stumped, because I never saw a hint in the release notes about "extension" suddenly being a keyword. It's easy to fix in my case by prefixing the reference to the extension class with an @ like this: public class eventChangeMatrimonialInheritanceArrangement
{
private @extension extensionField;
public @extension extension
{
get
{
return extensionField;
}
set
{
extensionField = value;
}
}
} Maybe this helps someone who stumbles into this issue, too. |
Beta Was this translation helpful? Give feedback.
-
The new extension syntax is more relaxed in naming restrictions than normal classes. class Greet
{
public void Hello() => Console.WriteLine("Hello");
public static void Hello() => Console.WriteLine("Hello");
// Error: Type 'Greet' already defines a member called 'Hello' with the same parameter types
} A similar restriction is enforced when creating an instance and static extension method: static class GreetExtensions
{
extension(string s)
{
public void Hello() => Console.WriteLine("Hello");
public static void Hello() => Console.WriteLine("Hello");
// Error: Type 'GreetExtensions' already defines a member called 'Hello' with the same parameter types
}
} However, If you put these extension methods in two separate classes, the compiler (VS 1714.2) has no issue with it: static class GreetExtensions1
{
extension(string s)
{
public void Hello() => Console.WriteLine($"Hello {s}"); // no error
}
}
static class GreetExtensions2
{
extension(string)
{
public static void Hello() => Console.WriteLine("Hello"); // no error
}
} That seems like an inconsistency. Is the original class restriction still needed. |
Beta Was this translation helpful? Give feedback.
-
With this feature I can finally write the public static class NullParseExtensions
{
extension<T>(T?) where T : struct, IParsable<T>
{
public static T? Parse(string? str, IFormatProvider? format = null) => string.IsNullOrEmpty(str) ? null : T.Parse(str, format);
}
} but, crushingly, it doesn't quite work: |
Beta Was this translation helpful? Give feedback.
-
Why not go with a less indented/verbose syntax? This is ugly IMO. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Hi, I tried this new feature with .NET 10 preview 5 and it's great! However I got errors when adding an operator extension according to the proposal (operator section). class Something<T> where T : INumber<T>
{
}
static class SomethingExtension
{
extension<T>(Something<T>) where T : INumber<T>
{
public static Something<T> operator +(Something<T> left, Something<T> right)
{
return new Something<T>();
}
}
} The errors are: Am I doing something wrong, or this feature has not been implemented? |
Beta Was this translation helpful? Give feedback.
-
Maybe time to get these
You hit these immediately when trying out the new extensions feature, but surprisingly few seem to complain. Code analysis, a rarely used feature? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Discussed in #5496
Originally posted by MadsTorgersen November 30, 2021
https://github.com/dotnet/csharplang/blob/main/proposals/extensions.md
Beta Was this translation helpful? Give feedback.
All reactions