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

Static classes #2270

Closed
nate-thegrate opened this issue Jun 1, 2022 · 8 comments
Closed

Static classes #2270

nate-thegrate opened this issue Jun 1, 2022 · 8 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@nate-thegrate
Copy link

Often times, a class is created as a way to organize a bunch of static functions/variables/constants rather than to be instantiated or extended. The Colors class is a great example: instead of creating objects of type Colors, you use its static members in place of Color objects.

Color mysteryColor  = Color(0xFF00FFFF); // not readable :(
Color favoriteColor = Colors.cyan;       // convenient!

Colors c = Colors(); // doesn't make sense

If you take a look at the colors.dart file, you'll see that they do something clever:

class Colors {
  // This class is not meant to be instantiated or extended; this constructor
  // prevents instantiation and extension.
  Colors._();
  ...
}

Since the abstract keyword doesn't prevent extension, it's necessary to have this empty private constructor. However, this isn't very intuitive, so you have to include that comment every time you make a static class.


Proposal

Allow classes to be defined using the static keyword.

For example, let's say we wanted to make a collection of Shape objects.

class Shape {
  int sides;
  Shape({required this.sides});
}

We could do something like this:

class Shapes {
  // This class is not meant to be instantiated or extended; this constructor
  // prevents instantiation and extension.
  Shapes._();

  static Shape circle = Shape(sides: 0);
  static Shape triangle = Shape(sides: 3);
  static Shape square = Shape(sides: 4);
  static Shape rect = Shape(sides: 4);

  static List<Shape> polygons = [triangle, square, rect];
  static List<Shape> get rectangles => [
        for (final polygon in polygons)
          if (polygon.sides == 4) polygon
      ];
}

But the class would be much more elegant if we could write it another way:

static class Shapes {
  Shape circle = Shape(sides: 0);
  Shape triangle = Shape(sides: 3);
  Shape square = Shape(sides: 4);
  Shape rect = Shape(sides: 4);

  List<Shape> polygons = [triangle, square, rect];
  List<Shape> get rectangles => [
        for (final polygon in polygons)
          if (polygon.sides == 4) polygon
      ];
}

This feature already exists in languages like C#, and I believe it would be a great addition to the Dart language.

@nate-thegrate nate-thegrate added the feature Proposed language feature that solves one or more problems label Jun 1, 2022
@jakemac53
Copy link
Contributor

jakemac53 commented Jun 1, 2022

@nate-thegrate do enhanced enums fit your use case?

void main() {
  print(Shape.values);
  print(Shape.polygons);
  print(Shape.rectangles);
}

enum Shape {
  circle(0),
  triangle(3),
  rect(4),
  square(4);
  
  final int sides;
  
  const Shape(this.sides);
  
  static List<Shape> get polygons => [triangle, square, rect];
  static List<Shape> get rectangles => [
        for (final polygon in polygons)
          if (polygon.sides == 4) polygon
      ];
}

The main issue is it doesn't allow for custom instances (for example Colors(0xFF00FFFF)), but that may actually be what you were looking for?

You do also need to specify static for any static members still, but that is because you can have both instance and static members, I think we would want that for static classes too (I doubt that we would want to force all members to be static).

@lrhn
Copy link
Member

lrhn commented Jun 1, 2022

Often times, a class is created as a way to organize a bunch of static functions/variables/constants rather than to be instantiated or extended.

The Dart style guide actively discourages that approach.

If you take a look at the colors.dart file, you'll see that they do something clever:

Adding a private constructor to prevent extension is "clever", but pointless. You can still implement the interface, and extending the class has absolutely no benefit to anyone (unlike Java, Dart doesn't "inherit" static members), so it's just adding code to prevent ... well, nothing. If someone wants to extend Colors, why spend time stopping them. They'll figure out soon enough that doing so is completely without any actual value.

What you are asking for here is a plain namespace. Dart class, mixin and extension declarations introduce a namespace for static methods. We could also introduce a namespace declaration which does nothing else, which I could have sworn we had an issue for already, but I can't find it. Since we discourage using such a namespace anyway, that's not a high priority.

For now, instead of class Colors { ... }, you can declare it as:

extension Colors on Never {
  // static members here.
}

That provides a namespace that can never be used for anything else. The only valid use of an extension name, other than designating a static namespace, is in an extension resolution override like Colors(o).member, but that requires both that o is assignable to type Never (leaving Never and dynamic) and that Colors has an instance member declaration named member, and it has none of those.

That's as close to a pure namespace as you can be today. It's pretty darn close too.

@nate-thegrate
Copy link
Author

Thanks to both @jakemac53 and @lrhn for the great responses. Enhanced enums fit a lot of use cases that you might want a static class for.

extension Colors on Never isn't very intuitive to read & understand, but it looks like it accomplishes exactly what I'm looking for. I also saw that the Dart style guide link you gave recommends using libraries instead:

If what you want is a namespace, a library is a better fit. Libraries support import prefixes and show/hide combinators.

Fortunately it looks like there are plenty of different ways to accomplish what I'm trying to do, so I think we're good to close the issue.

@Levi-Lesches
Copy link

@lrhn I believe you're thinking about #2254 (comment), as well as a lot of namespace-related talk in #336

@lrhn
Copy link
Member

lrhn commented Jun 2, 2022

Thanks @Levi-Lesches, there doesn't seem to be a single issue just about namespaces, but it keeps cropping up in other discussions.

The extension Colors on Never is definitely not intuitive.

A namespace would be like a class, mixin or extension declaration that cannot be used for what those declarations are otherwise used for. The extension ... on Never satisfies that by not introducing a type or interface to begin with, and being entirely unusable for what extensions or otherwise used for. Using a class or mixin would at least introduce an interface.
Not pretty, but useless!

@nate-thegrate
Copy link
Author

For those interested, I've created #2272 based on what's been discussed in this issue.

@mateusfccp
Copy link
Contributor

With the upcoming class modifiers will abstract final class be an alternative on extension ... on Never? I don't think it's any more intuitive, but it will be at least "documented".

@munificent
Copy link
Member

Yeah, I think abstract final class gets you most of the way there. It's a class that you can't construct, extend, mix in, or implement.

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

6 participants