language icon indicating copy to clipboard operation
language copied to clipboard

Proposal: `base` class members

Open nate-thegrate opened this issue 6 months ago • 4 comments

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

  • Uses existing keyword (stable could be a variable name)
  • Supports type promotion for non-final variables
  • Allows a class field to be used in a constant context (resolves #299)

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 #2225)

nate-thegrate avatar Aug 16 '24 18:08 nate-thegrate