Skip to content
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

Proposal: base class members #4041

Open
nate-thegrate opened this issue Aug 16, 2024 · 4 comments
Open

Proposal: base class members #4041

nate-thegrate opened this issue Aug 16, 2024 · 4 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@nate-thegrate
Copy link

nate-thegrate commented Aug 16, 2024

Overview

I recently learned that the abstract keyword can be used for class members:

abstract class Foo {
  abstract final Object? a;
  abstract Object? b;
}

This issue proposes using the base keyword in a similar way:

class Foo {
  const Foo({this.a});
  base final Object? a;
}

class Bar extends Foo {
  Bar({super.a, this.b});
  base Object? b;
}

When you implement a class, you override all of its fields, whereas when you extend a class, you can choose to override fields individually.

Like how a base class prevents implementation, a base field within a class prevents overriding that field.

class A {
  const A({this.value});
  base final int? value;
}

class B extends A {
  @override
  int get value => 42; // compile-time error, cannot override a base field
}

Detailed rules (click to expand)

Used for class fields

base int value = 42; // error: not in a class declaration


base class A {
  const A(this.value);
  base final int value; // OK

  void foo() {
    base int value = 42; // error: does not apply to local variables
  }
}

class B {
  const B(this.value);
  base final int value; // OK, does not need to be inside a base class
}

class C {
  base int i = 0; // OK, can be used for non-final variables

  late base Object data; // OK, works for "late" values

  base void foo() {
    // OK, can be used for methods
  }

  base int get value => 42; // OK, but getters won't gain benefits
                            // as described further down
  base set value(int? newValue) {
    // OK, works for setters
  }
}

Does not apply to abstract or static fields

abstract class A {
  abstract base Object value; // error: abstract base member

  base String get label; // error: abstract base getter

  base void foo(); // error: abstract base method

  static base A of(BuildContext context) {
    // error: static base member
  }
}

Don't override inherited base fields

class A {
  base void foo() {
    print('I love Dart!');
  }
}

class B extends A {
  @override
  void foo() { // error: cannot override an inherited base field
    print('hello');
  }
}

Can override non-base fields

class A {
  const A({this.value});
  final Object value;
}

class B implements A {
  @override
  base int value = 0; // OK
}

abstract class C {
  Object? get data;
}

class D extends C {
  const D({this.data});

  @override
  base final Object data;
}

Implementing a class with a base field

A base class member can be overridden when implementing the class, but the new value must also have the base modifier. A base member cannot be replaced with a getter.

class A {
  const A(this.value);
  base final Object value;
}

class B implements A {
  const B({this.value = ''});

  @override
  base final String value; // OK
}

class B implements A {
  const B({this.value = ''});

  @override
  final String value; // error: value must have "base" modifier
}

class C implements A {
  @override
  base int value = 0; // OK
}

class D implements A {
  @override
  base int get value => 0; // error: member cannot be changed to getter
}

When the class is implemented, a base getter or method can be overridden by another base getter/method with no additional restrictions. To prevent overriding, use a base class.

import 'dart:math' as math;

class A {
  base String get coinFlip {
    return math.Random().nextBool() ? 'heads' : 'tails';
  }
}

class B implements A {
  @override
  base String get coinFlip => 'always tails'; // OK
}

base class C {
  base String get coinFlip {
    // cannot be overridden, unless it's implemented in the same library
    return math.Random().nextBool() ? 'heads' : 'tails';
  }
}

Benefits

Type promotion

A base class member qualifies for type promotion, as if it were a local variable.

class A {
  const A({this.value});
  base final Object? value;

  void foo() {
    if (value is String) {
      print(value.substring(2));
    }
  }
}

class B {
  B({this.value});
  base Object? value;

  void foo() {
    if (value is int) {
      value += 3;
    }
  }
}

Constant class fields

If foo is a constant value, and bar is a base final field in its class declaration, then foo.bar can be used in a constant context.

class Fraction {
  const Fraction(this.numerator, this.denominator)
      : value = numerator / denominator;

  base final num numerator, denominator;
  base final double value;
}

const fraction = Fraction(5, 4);
const remainder = fraction.value % 1;

Discussion

This proposal is closely related to #1518, but has a few differences.

Advantages of base

Advantages of stable

  • Can be applied to local, global, and static fields
  • Supports type promotion for getters
  • Since a stable getter can override a stable field, it can be used in place of a late final value, allowing a class declaration to keep its const constructor (though this could also be achieved via Allow late final fields on const classes #2225)
@nate-thegrate nate-thegrate added the feature Proposed language feature that solves one or more problems label Aug 16, 2024
@Levi-Lesches
Copy link

This would basically be a language-level @nonVirtual?

@nate-thegrate
Copy link
Author

Yeah that's right! (I didn't know about @nonVirtual till now, but it looks like both of these follow the same idea.)

I don't want to pull attention away from more important things—specifically macros & union types—but it could be nice to have something intuitive for nonlocal type promotion.

@Levi-Lesches
Copy link

One thing I should point out is that base does not necessarily mean "cannot override":

base class A {
  A() { print("This will always print"); }

  void test() => print("This can be overridden");
}

base class B extends A { 
  @override
  void test() => print("A.a() is replaced by B.test()");
}

base just means you can't implement the class, which is relevant mostly for two reasons:

  • the constructor is never overridden
  • any private members are automatically inherited
  • private members cannot be overridden outside that file, but they can be overridden when in the same file

But any public members certainly can be overridden, so I'm not sure using base is the correct connotation here.

@nate-thegrate
Copy link
Author

nate-thegrate commented Sep 13, 2024

@Levi-Lesches fair points, though I do think that "ensure a base method won't be overridden by putting it in a base class" could be a valuable intuition.


Or in other words…

  • interface class: "cannot inherit, must override"
  • base class with base field: "cannot override, must inherit"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

2 participants