language icon indicating copy to clipboard operation
language copied to clipboard

Static extension methods

Open rrousselGit opened this issue 5 years ago • 110 comments

Motivation

Currently, extension methods do not support adding static methods/factory constructors. But this is a missed opportunity!

There are many situations where semantically we want a static method/factory, but since the type is defined from an external source, we can't.

For example, we may want to deserialize a String into a Duration.

Ideally, we'd want:

extension ParseDuration on Duration {
  factory parse(String str) => ...;
}

Duration myDuration = Duration.parse('seconds: 0');

But that is currently not supported. Instead, we have to write:

Duration parseDuration(String str) => ...;

Duration myDuration = parseDuration('seconds: 0');

This is not ideal for the same reasons that motivated extension methods. We loose both in discoverability and readability.

Proposal

The idea is to allow static and factory keyword inside extensions.

Factory

Factories would be able to capture the generic type of the extended type, such that we can write:

extension Fibonacci on List<int> {
  factory fibonacci(int depth) {
    return [0, 1, 1, 2, 3, 5];
  }
}

Which means we'd be able to do:

List<int>.fibonacci(6);

But not:

List<String>.fibonacci(6);

Factories would work on functions and typedefs too (especially after #65):

typedef MyEventHandler = void Function(Event event);

extension MyShortcut on MyEventHandler {
  factory debounced(Duration duration, MyEventHandler handler) {
    return (Event event) { .... } 
  }
}

Static members

Using extensions, we would be able to add both static methods and static properties.

They would not have access to the instance variables, nor the generic types. On the other hand, static constants would be allowed.

We could, therefore, extend Flutter's Colors to add custom colors:

extension MyColors on Colors {
  static const Color myBusinessColor = Color(0x012345678);
}

Or Platform to add custom isWhatever:

extension IsChrome on Platform {
  static bool get isChrome => ...;
}

which, again, would work on functions and typedefs

typedef MyEventHandler = void Function(Event event);

extension MyShortcut on MyEventHandler {
  static void somePremadeHandler(Event event) {}
}

rrousselGit avatar Dec 06 '19 08:12 rrousselGit

If you have ParseDuration.parse as a "constructor", it would be kind of misleading if that was available as Duration.parse.

That wouldn't be the case. We currently can use the static keyword, but not factory.

MyExtension.someFactory shouldn't be a thing

rrousselGit avatar Dec 06 '19 14:12 rrousselGit

There's still a difference between static methods and factories.

Factories can do:

abstract class Foo {
  const factory Foo.myFactory(int a) = _FooImpl;
}

class _FooImpl implements Foo {
  const _FooImpl(int a);
}

Which is the only way to have const constructors on factory.

That's a bit off-topic though.

rrousselGit avatar Dec 06 '19 15:12 rrousselGit

As has been pointed out, static is already allowed in extensions, and it creates static methods in the extension namespace, not on the on-type.

Class declarations introduce a namespace too, and for a generic class, the distinction between a type and the class is fairly big. The class declaration List is not represented by the type List<int> or even List<dynamic> (or in the future: List<dynamic>?). Adding static methods or constructors to a class namespace would need to be declared on a class rather than a type, which is what the on type of an extension declaration is.

That is one reason for not allowing static methods of extension declarations to be accessible on the on type: The on type of an extension declaration can be any kind of type, not just class types. There is not necessarily a namespace that the methods can go into. Putting a static method on int Function(int) or List<int> (but not List<String>) would leave you with no way to call that static method.

We could consider new syntax to actually inject static members into other classes, say static extension on List { int get foo => 42; }, which would only allow a class name as on target.

[!NOTE] If we have extension statics, then it's possible to declare a static member with the same name as an instance member on the same type, as long as at least one is an extension member. We should probably allow declaring that inside the same class declaration, rather than pushing people to use extensions to achieve the same thing indirectly. (Maybe augmentations can allow the same thing, which is another argument for just allowing it directly.) #1711

lrhn avatar Dec 10 '19 07:12 lrhn

That request doesn't ask for allowing static methods on List<int> though, but factory.

I do agree that it doesn't make sense to use the static keyword on a generic on. But I think it does make sense for factory.

Does your comment apply to factory too?

rrousselGit avatar Dec 10 '19 07:12 rrousselGit

Adding factory to a class would be a really nice thing to have. In terms of real world examples it would be nice for browser apps when doing CustomEvents.

extension MyEvent on CustomEvent {
  factory CustomEvent.myEvent(String message) {
    return CustomEvent('my-event', message);
  }

  String get message {
    assert(type == 'my-event', 'CustomEvent is not 'my-event');
    return detail as String;
  }
}

I was also thinking of using it for a http like library.

extension JsonRequest on Request {
  factory Request.json(Map<String, dynamic> body) {
    return Request(jsonEncode(body), headers: { 'Content-Type': 'application/json' });
  }
}

donny-dont avatar Dec 13 '19 01:12 donny-dont

A factory declaration is not as bad as a pure static method because it does provide a way to give type arguments to the class. It's still vastly different from an extension method.

If you define:

extension X on Iterable<int> {
  factory X.fromList(List<int> integers) => ... something ...;
}

then what would it apply to? Most likely it would only apply to Iterable<int>.fromList(...). That means no subtyping, because List<int>.fromList(...) would probably not create a list, only an iterable. But perhaps super-types, because Iterable<num>.fromList([1, 2, 3]) would create an iterable of numbers. That's completely different from how extension methods are otherwise applied, where the method applies to any subtype of the on type, so again I think it deserves its own feature. It's not just an extension of extension.

The on type of that feature must be a class because you can only write new ClassName<typeArgs>(args) to invoke it, and there is no syntax which allows this on int Function(int).

All in all, I'd prefer something like:

static extension Something<T> on ClassType<T> {
  static int method() => 42;
  factory Something.someName() => new ClassType<T>.actualConstructor();
}

which effectively adds static members and constructors to a class declaration, and can be hidden and imported just like extensions.

It can probably do one thing normal constructors cannot: Declare a constructor only on a subtype, so:

static extension X on List<int> {
  factory X.n(int count) => List<int>.filled(count, 0);
}
... List<int>.n(5) ...

(I would support adding extra restrictions to type arguments in constructors in general too.)

lrhn avatar Jan 02 '20 13:01 lrhn

Nice catch on the subclass thing.

For me, factory/static extensions should be applied only on the extact match of the on clause.

Such that with:

static extension on A {
  factory something() = Something;

  static void someMethod() {}
}

Would be applied only on A but not on a subclass of A. This would otherwise introduce a mecanism of static method inheritance, which gets kinda confusing and likely unexpected.

The on type of that feature must be a class because you can only write new ClassName<typeArgs>(args) to invoke it, and there is no syntax which allows this on int Function(int).

What about requiring a typedef, and applying the extension only typedefs then?

Because typedefs makes it definitely useful. I would expect being able to write:

typedef VoidCallback = void Function();

static extension on VoidCallback {
  static empty() {}

  const factory delayed(Duration duration) = CallableClass;
}

VoidCallback foo = VoidCallback.empty;
const example = VoidCallback.delayed(Duration(seconds: 2));

Currently, Dart lacks a way to namespace functions utils

rrousselGit avatar Jan 02 '20 14:01 rrousselGit

@tatumizer With such syntax, how would you represent generic extensions?

static extension<T extends num> on List<T> {
  factory example(T first) => <T>[first];
}

rrousselGit avatar Jan 02 '20 15:01 rrousselGit

If the static extension itself does not have a name, then it's not possible to hide it if it causes conflicts. That's why extension declarations have names. I'd want that for static extensions too.

lrhn avatar Jan 06 '20 10:01 lrhn

I think static extension methods would somewhat help with #746. However, there may be a couple of things that might be confusing, like how type inference affects constructor availability.

e.g.

static extension IntList on List<int> {
  factory List<int>([int length]) => List.filled(length ?? 0, 0);
}

static extension NullableList<T> on List<T?> {
  factory List<T?>([int length]) => List.filled(length ?? 0, null);
}

etc.

For constructor availability, it feels weird to have to specify the type to get the correct factory constructors. For example:

List<int> foo;

foo = List(123);

Also, if you have to disambiguate between multiple extensions, how would the syntax look? IntList(123)? This expands to new IntList(123), but the return type isn't IntList (because it isn't a type, it's a member).

ds84182 avatar Jan 06 '20 18:01 ds84182

I just wanted to voice support for adding static members to an existing class. For what it's worth I don't care about factory constructors since static methods are virtually indistinguishable at call sites anyways.

For context, I think this would help improve certain code generators that want to generate top-level members based on an existing types. Rather than requiring users to remember a particular naming convention to find these members, they could instead be added as extensions to their associated class.

For example, AngularDart compiles developer-authored components into a number of view classes. We expose a ComponentFactory instance from the generated code that is used to instantiate a component and its corresponding view dynamically:

// Developer-authored
// example_component.dart

@Component(...)
class ExampleComponent { ... }
// Generated
// example_component.template.dart

ComponentFactory<ExampleComponent> createExampleComponentFactory() => ...;
// main.dart

import 'example_component.template.dart';

void main() {
  // User has to remember this naming convention.
  runApp(createExampleComponentFactory());
}

Ideally we could declare these ComponentFactory functions as static extension methods on their corresponding component class instead of putting them in the top-level namespace. I think this offers better ergonomics and readability:

// Generated
// example_component.template.dart

static extension ExampleComponentFactory on ExampleComponent {
  ComponentFactory<ExampleComponent> createFactory() => ...;
}
// main.dart

import 'example_component.template.dart';

void main() {
  runApp(ExampleComponent.createFactory());
}

leonsenft avatar Feb 07 '20 23:02 leonsenft

I was just adding extensions to one of my library.

first i tried to move factory methods to an extension but got error that factory are not supported and i was like that's fine.

Then tried to convert factory constructor to static functions with factory annotations, the IDE did not complain but the client code using the extension did complain.

I was expecting static methods to be supported because most of the language i have used support it...

bitsydarel avatar Feb 13 '20 22:02 bitsydarel

Also the current behavior of defining static extension methods is not clear.

example:

Previous API definition.

class ExecutorService {
  factory ExecutorService.newUnboundExecutor([
    final String identifier = "io_isolate_service",
  ]) => IsolateExecutorService(identifier, 2 ^ 63, allowCleanup: true);
}

With the new extension feature.

extension ExecutorServiceFactories on ExecutorService {
  @factory
  static ExecutorService newUnboundExecutor([
    final String identifier = "io_isolate_service",
  ]) => IsolateExecutorService(identifier, 2 ^ 63, allowCleanup: true);
}

Currently the client code have to call it this way:

ExecutorServiceFactories.newUnboundExecutor();

But i think the syntax should be:

ExecutorService.newUnboundExecutor();

It's more common for people that are use to extensions in other languages, it's more clear on which type its applied, its does not create confusion in client code, it's does not the break the API of library and does not require code change on client side.

bitsydarel avatar Feb 13 '20 22:02 bitsydarel

I'm not sure if it has been mentioned yet. Adding this support would increase the set of changes which are breaking.

As of today it is not a breaking change to add a static member on a class. If we give the ability to add members to the static interface of a class we need to come up with how to disambiguate conflicts. If we disambiguate in favor of the class it's breaking to add a static member, because it would mask a static extension. I'm not sure all the implications of disambiguating in favor of the extension.

natebosch avatar Feb 14 '20 00:02 natebosch

As has been pointed out, static is already allowed in extensions, and it creates static methods in the extension namespace, not on the on-type.

At this point, the extension is useless, since it's just a class with static members. for calling PlatformX.isDesktop, the following 2 snippets produce the same results.

extension PlatformX on Platform {
  static bool get isDesktop =>
      Platform.isMacOS || Platform.isWindows || Platform.isLinux;
  static bool get isMobile => Platform.isAndroid || Platform.isIOS;
}
class PlatformX {
  static bool get isDesktop =>
      Platform.isMacOS || Platform.isWindows || Platform.isLinux;
  static bool get isMobile => Platform.isAndroid || Platform.isIOS;
}

ThinkDigitalSoftware avatar Mar 15 '20 20:03 ThinkDigitalSoftware

The purpose of using extensions for static methods and factories is to regroup everything in a natural way.

Sure, we could make a new placeholder class. But then you have to remember all the possible classes that you can use.

rrousselGit avatar Mar 15 '20 20:03 rrousselGit

The purpose of using extensions for static methods and factories is to regroup everything in a natural way.

Sure, we could make a new placeholder class. But then you have to remember all the possible classes that you can use.

regroup everything in a natural way Yes, that's the point!👍


I found this problem when making a flutter plugin. I plan to put static methods and static callback Function members in the same class for the convenience of users, but on the other hand, I want to move the callback to another file to Improve readability.

I found that dart 2.6 supports extensions. I thought it was similar to swift, but when I started to do it, I found various errors. After searching, I regret to find that static method extensions are not supported.🥺

extension ZegoEventHandler on ZegoExpressEngine {
    static void Function(String roomID, ZegoRoomState state, int errorCode) onRoomStateUpdate;
}
  void startLive() {
    // Use class name to call function
    ZegoExpressEngine.instance.loginRoom("roomID-1");
  }

  void addEventHandlers() {
    // I want to use the same class to set the callback function, but it doesn't work
    // ERROR: The setter 'onRoomStateUpdate' isn't defined for the class 'ZegoExpressEngine'
    ZegoExpressEngine.onRoomStateUpdate =
        (String roomID, ZegoRoomState state, int errorCode) {
      // handle callback
    };

    // This works, but requires the use of extended aliases, which is not elegant
    ZegoEventHandler.onRoomStateUpdate =
        (String roomID, ZegoRoomState state, int errorCode) {
      // handle callback
    };
  }

At present, it seems that I can only use the extension name to set the callback function, which can not achieve the purpose of letting the user only pay attention to one class.🧐

patrick-fu avatar Mar 28 '20 19:03 patrick-fu

Definitely agree that the static method should be called from the original class and not the extension class.

jlubeck avatar Apr 02 '20 20:04 jlubeck

@rrousselGit I think the same, in my case the intention is to use to apply Design System without context doing this way:


extension ColorsExtension on Colors {
  static const Color primary = const Color(0xFFED3456);
  static const Color secondary = const Color(0xFF202426);

  static const Color backgroundLight = const Color(0xFFE5E5E5);
  static const Color backgroundDark = const Color(0xFF212529);

  static const Color warning = const Color(0xFFFFBB02);
  static const Color warningBG = const Color(0xFFFFFCF5);
  static const Color confirm = const Color(0xFF00CB77);
  static const Color confirmBG = const Color(0xFFEBFFF7);
  static const Color danger = const Color(0xFFF91C16);
  static const Color dangerBG = const Color(0xFFFEECEB);

  static const MaterialColor shadesOfGray = const MaterialColor(
    0xFFF8F9FA,
    <int, Color>{
      50: Color(0xFFF8F9FA),
      100: Color(0xFFE9ECEF),
      200: Color(0xFFDEE2E6),
      300: Color(0xFFCED4DA),
      400: Color(0xFFADB5BD),
      500: Color(0xFF6C757C),
      600: Color(0xFF495057),
      700: Color(0xFF495057),
      800: Color(0xFF212529),
      900: Color(0xFF162024)
    },
  );
}

faustobdls avatar Apr 05 '20 13:04 faustobdls

@faustobdls In that case, it seems pretty counterintuitive to me to do what you are proposing for these two reasons:

  1. If Colors.primary is used in your code, it might appear as though the material Colors class declares this primary color. However, this is not the case! You declare it for you own design yourself. Why do you not give your class a more telling name instead of wanting to add to Colors, like CustomDesignColors. You could even make that a mixin or extension with the current implementation.

  2. What should happen when the material Colors class is updated and now declares members with the same names?

@creativecreatorormaybenot my intention is the use of this for flavors within the company, definition of design system and etc, besides that this is just a case, we can mention others that depend on the fact that the static and factory methods can be used by others , recent we wanted to make a class Marker a toJson () and a fromJson () and fromJson () is static we had to create another class for this, but with extension it would be much better and more readable, at least in my opinion

faustobdls avatar Apr 05 '20 14:04 faustobdls

Any news? Very necessary feature

listepo avatar May 21 '20 21:05 listepo

@creativecreatorormaybenot Extension methods as a whole are convenience/style feature. Since they were implemented they should be implemented right, and this is missing. Also nobody said it would be prioritized over NNBD. Don't worry, NNBD will not be delayed because of this issue, if that's what you fear, somehow.

marcglasberg avatar May 21 '20 23:05 marcglasberg

Another idea.

How about allowing extensions on multiple types, and on static declaration names, in the same declaration:

extension Name 
   <T> on List<T> {
      .... normal extension method ...
   } <T> on Iterable<T> { 
      ...
   } <K, V> on Map<K, V> {
     ...
  } on static Iterable { // static methods.
    Iterable<T> create<T>(...) => 
  } 

I'd still drop constructors. It's to complicated to make the distinction between a constructor on a generic class and a generic static method. Allowing that does not carry its own weight.

By combining the declarations into the same name, we avoid the issue of having to import/export/show/hide two or more names for something that's all working on the same types anyway.

Having the same name denote different extensions does make it harder to explicitly choose one. If I do Name(something).method(), it will still have to do resolution against the possible matches. Is it a list or iterable? What id it's both a map and a list?

lrhn avatar May 24 '20 09:05 lrhn

There is another unexpected problem extension methods cause, but this is also one they could, with @rrousselGit proposal, fix. The problem is that extension methods only exist when the generic type argument is known. But in a generic class' constructor we don't know the concrete type. I try to explain: I was trying to write a thin wrapper type for Dart's FFI:

class Array<T extends NativeType> {
  Pointer<T> ptr;
  List _view;
  Array(int length) // Problem: ptr.asTypedList() only exists with known type T
}

Pointer<T> has various extensions like Uint8Pointer on Pointer<Uint8> which defines asTypedList() -> Uint8List. But in the constructor of Generic<T> I don't know the concrete type, so I can't call asTypedList(). This would be trivial to solve with C++ templates, but Dart makes this trivial seeming problem very difficult to solve.

With @rrousselGit proposal this problem could be easily solved:

class Array<T extends NativeType> {
  Pointer<T> ptr;
  List _view;
  Array._(this.ptr, this._view);
}
extension Uint8Array on Array<Uint8> {
  Uint8List get view => _view;
  factory Array(int length) {
    final ptr = allocate<Uint8>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}
extension Int16Array on Array<Int16> {
  Int16List get view => _view;
  factory Array(int length) {
    final ptr = allocate<Int16>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}

Array<Uint8>(10); // works
Array<Int16>(10); // works
Array<Uint32>(10); // doesn't work
Array(10); // doesn't work

But for now I have to use static extensions which are a heavy burden on users:

extension Uint8Array on Array<Uint8> {
  Uint8List get view => _view;
  static Array<Uint8> allocate(int length) {
    final ptr = allocate<Uint8>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}
extension Int16Array on Array<Int16> {
  Int16List get view => _view;
  static Array<Int16> Array(int length) {
    final ptr = allocate<Int16>(count: length);
    return Array._(ptr, ptr.asTypedList(length));
  }
}

Uint8Array.allocate(10);
Int16Array.allocate(10);

This is just confusing because a user would think Uint8Array would be another type. And now he has to remember all these extensions to create an Array.

Subclassing would not be a good solution, because then Array<Uint8> couldn't be passed where Uint8Array is expected. They would be incompatible. Java programmers make this mistake all the time because they lack type aliases. In fact, it would be really cool if we could use extensions as type aliases:

Uint8Array arr = Uint8array.allocate(10); // synonymous to Array<Uint8> arr;

MarvinHannott avatar Jul 12 '20 16:07 MarvinHannott

I have the same issue while wanting to add static method as extension to the User model of sqflite.

I would like to have something like this:

User user = await User.get(db);

But currently I have to:

User user = await User().get(db);

omidraha avatar Jul 21 '20 15:07 omidraha

I have the same issue when trying to create a extension function from network images. If we consider dart as a serious language, this feature should be implemented.

jpgpuyo avatar Sep 13 '20 20:09 jpgpuyo

It will also help with cleaner code generation I like to use extension for code generation (for example mutable_copy, copy_with) Support of static, factory (and even override) for extension will add more possibilities for code generation libraries.

Just a sample:

@JsonSerializableExtension()
@EquatableExtension()
@imutable
@MutableCopy()
@CopyWith()
// And many more possible extension
class User {
  final String name;
  final String email;

  User({this.name, this.email});
}

generate code:

extension $UserJsonSerializableExtension on User {
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

extension $UserEauatableExtension on User {
  @override
  bool operator ==(Object other) => _$UserEqual(this, other);

  @override
  int get hashCode => _$UserHashCode(this);
}

...

Model will looks cleaner.

We even will have ability to implement equivalent of kotlin data class with one annotation.

kotlin

data class User(val name: String, val email: String)

dart

@DataModel()
class User {
  final String name;
  final String email;

  User({this.name, this.email});
}

AlexVegner avatar Sep 21 '20 09:09 AlexVegner

Just wanted to provide a concrete example of where I would use this.

extension SizedBoxX on SizedBox {
  static SizedBox square(double dimension, {Widget child}) => SizedBox.fromSize(
    size: Size.square(dimension),
    child: child,
  );
}
return SizedBox.square(16);

lukepighetti avatar Oct 05 '20 14:10 lukepighetti

I did a quick write-up of a proposal strawman, which tries to address at least one of the pain points of having to declare extension static members separately from extension instance members (by allowing the extensions to share the same name).


Dart Extension Static Methods

Dart has static extension methods which allows declaring static functions which can be invoked as if they were non-virtual instance methods on specific types. Users have requested a way to also add extension static methods to specific classes, which the current feature does not provide.

This is a proposal for a generalized syntax for extensions which include a way to add static functions which can be invoked as if they were static members of a specific namespace declaration.

Extension Static Methods

The syntax for extension (instance) methods is:

extension Name<T> on SomeType<T> {
  ... instanceMethod() ...
}

We will introduce the syntax:

extension Name on static DeclName<T> {
  ... static looksLikeStaticMethod() ...
  ... factory Name.name() ... // Factory constructor with return type DeclName<T>
  ... factory Name() ... // Empty-named constructor too.
  ... static const myConst = constExpr;
}

The name after static must be the name of a class or mixin declaration. (We do not allow extension statics to be declared on another extension declaration, which is the third place where you can currently declare static members.) The static declarations must be static method, getter or setter declarations, static constant declarations, or factory constructor declarations, are then made available as DeclName.looksLikeStaticMethod(), DeclName.myConst or new DeclName.name().

For factory constructors, both DeclName and Name are allowed as the name before the .. (I find Name more correct, but really, really want to write DeclName when I write the code. If we changed the language syntax to allow new, instead of repeating the class name, for all constructors, then that headache would go away!)

The type parameter on the class is only necessary when using a constructor, it gives a name to the type provided in new DeclName<int>.name(), and is available inside the constructor. It’s not available in static methods. Another alternative would be writing it as factory DeclName<T>.name() …. In that case, it makes little sense to use Name instead of DeclName since it’s DeclName‘s type parameters we refer to.

If DeclName already has a static member with the same base name, then it takes precedence over the extension. NOTICE: An instance member does not shadow the extension. We will get back to this.

If two extension statics both add static members with the same name on the same declaration, accessing that name is a compile-time error unless all-but-one of them are declared in platform libraries.

The static members are also available as static members of Name, so you can invoke Name.looksLikeStaticMethod() and new Name.name() directly to circumvent naming conflicts (or just not import one of the extensions as usual).

Combined Extensions

It will likely be common to declare both static and instance extensions on (roughly) the same type, and it’s already common to declare multiple related extensions on different types. It’s annoying to have to hide or show more than one name, so we allow multiple extensions to share the same name.

extension Name on static DeclName {
  static ...
} <T> on DeclName<T> {
  ...
} <T> on SubOfDeclName<T> {
  ...
} on static SubOfDeclName {
  static ... 
}

These are treated as separate extensions for resolution, but are all hidden/shown as one.

Since the extensions have the same name, we now need conflict resolution for the explicit extension application syntax: Name(e).foo. There may now be more than one extension named Name which applies, and we’ll use the normal extension resolution to choose between those (and potentially fail). It’s up to the individual combined extension author to not create situations where conflict resolution will fail.

Another option is to only allow at most one one non-static section in each declaration. Or even only allow at most one non-static and at most one static section, with the intent that the two should be on the “same” type. That reduces or removes the risk of name conflicts.

Same-named static and instance members

Currently Dart does not allow a class to have a static member and an instance member with the same base name. If a class’s interface declares or inherits an instance member, it cannot declare a static member with the same base name. Since static members are not inherited, a subclass can declare a new member with the same name as a superclass static member.

With extension static members, you can do something which resembles that:

class C {
  int get foo => 42;
}
extension CFoo on static C {
  int get foo => 37;
}
void main() {
  print(C().foo); // 42
  print(c.foo); // 37
}

Giving authors this power indirectly risks them using it, declaring a class and a static extension on the class right next to each other. The only real difference between this and allowing the class to declare the static directly is the added risk of another extension conflicting with the name, which such authors will likely ignore.

That suggests that we should allow a class to declare a static member and an instance member with the same base name, rather than making that remain a conflict. That is:

  • A class can declare an instance member and a static member with the same base name (or two of the same kind if it’s a setter/getter pair, as usual).
  • A static member body, non-late field initializer, or initializer list entry resolves such a name to the static declaration.
  • An instance member body, late field initializer, or constructor body resolves the name to the instance member.

If there is only one declaration, the name always refers to that, so it’s not exactly like having two nested scopes. It’s more like a scope with two possible values for each name, and a lookup which prefers one over the other.

Examples

extension MyEnumX on static MyEnum {
  factory MyEnumX.fromString(String name) => 
      MyEnum.values.firstWhere((s) => s.endsWith(".$name"));
} on MyEnum {
  MyEnum get next => MyEnum.values[(this.index + 1) % MyEnum.values.length];
}

lrhn avatar Oct 08 '20 12:10 lrhn