-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Missing possibility to associate values to keys in the enum. #33698
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
Comments
Unlikely to be "near future". This has been requested for a while, at least since #21966. What you are asking for here is really just integer constants and a type alias for them. The constants we can already do: class Role {
static const NormalUser = 1;
static const Admin = 2;
static const Fiance = 4;
static const Bob = Fiance & Admin;
} |
Perhaps just adding support for |
I can live without the feature- seems like some people are actually against this use-case scenario (which does not make sense to me), but if there is a roadmap with small help I think I would be able to add this feature to darts' vm. |
I'm not opposed to having value-based enums in addition to class based enums. I'd also like to have more expressive class based enums (like Java's). I even think it's possible to have both, by making value-based enums use enum Foo { // value based, first entry needs `=` or defaults to zero.
foo = 42,
bar, // defaults to 43, works for integer values only.
baz = 37
}
enum Bar implements Comparable<Bar> { // class based
foo,
bar,
baz
int compare(Bar other) => this.index - other.index;
} All in all, I just don't think value-based enum syntax brings a lot of value compared to the existing alternative (static constants), so I don't see it as a high priority. |
@lrhn this solution are not suit for type check. Think this scene. enum Speed {
SLOW = 0.5,
MEDIUM = 1,
FAST = 1.5
}
class Vehicle {
final Speed _speed;
Vehicle({ this._speed = Speed.SLOW });
} Class is not fit this type check scene. |
@izayl wrote:
If you want to use a Dart doesn't have that (in particular But we have discussed similar ideas before. It's not obvious that a proper branding mechanism in the type system would carry its own weight, but we could try to support a similar kind of static discipline outside the type system (for example, #57828, which could serve as a plug-in framework for many different kinds of static checking, with branded types as a starting point). |
We really need this ability to associate values with enums. There are many times when I need to obtain a list of keys or a list of values from an enum. For example:
For my UI, I would want to use eLanguage.keys as my dropdown items. For my supportedLocales in my MaterialApp, I would use eLanguage.values. If I am forced to use static constants, I would have to create 'values' and 'keys' properties in my class and work hard to keep my actual enums and the lists returned from these properties in sync with the actual enums defined. This is a poor solution. |
Please add this feature. Enum values are very useful |
We also use enums in different ways (backend and frontend):
it's very useful to add this feature in dart. |
While not ideal, couldn't you now technically solve this with extension methods?
|
You could use Maps for that (the only limitation would be not having them as constants). But from my understanding, that's what Maps are exactly for.
So, in your case, it would be:
|
The problem is, that maps are accessed by keys which are not type safe in the way enums are. Also autocompletion and so on is very bad. |
only dart can't solve this like TypeScript. so currently i used extensions for this:
in this case the getter is nice:
i'm not like the toAppValue function, because i must to this:
but i hoped, i can do something like this:
or do you know a better way? |
You can do: extension MyAppEnumFromValue on int {
MyAppEnum toAppValue() => MyAppEnum.toAppValue(this);
} (Your |
As @izayl already stated while class constants work fine for simple comparison/switch scenarios, it's not a solution for type safety, casting and bit wise value handling. I recently encountered the casting issue during parsing.
Deserializing it with most json deserializer libraries would map the integer to the proper enum value. And doing it manually would require a simple int->MyModeEnum cast (or equivalent). While using extension methods and such for working around the current limitations of Dart is possible, it's a lot of potential code clutter. |
this is how I use it to mimic Swift enums |
IMHO, those who comes from C++ / C# will appreciate how important and flexible to be able to associate an Enum with Int. No, not a class / dictionary / maps representation of enums. If we wish to remain using class / dictionary / maps, we can still do it that way, but there is a reason why enum exist. For the long term growth of dart, please consider it seriously! Enum is one of the horrible implementation keeps me away from Java. Besides the pathetic type eraser in Generics. |
This is not an ideal solution as it loses the static analysis benefits you get with enums. (e.g. you won't get a warning in a |
With extension it is even not so verbose :-) enum Role { NormalUser, Admin, Fiance, Bob }
extension RoleEx on Role {
int get value {
switch (this) {
case Role.NormalUser:
return 1;
case Role.Admin:
return 2;
case Role.Fiance:
return 4;
case Role.Bob:
return Role.Fiance.value & Role.Admin.value;
}
return null;
}
} And all case statements of switch are generated by IDE, so you don't have to type them. Or another way, though no linter warning in case of not all enum values are populated in _map: enum Role { NormalUser, Admin, Fiance, Bob }
extension RoleEx on Role {
static final Map<Role, int> _map = {
Role.NormalUser: 1,
Role.Admin: 2,
Role.Fiance: 4,
Role.Bob: _map[Role.Fiance] & _map[Role.Admin]
};
int get value => _map[this];
} |
If it were implemented natively without extensions, it would look like this: enum Role { NormalUser = 1, Admin = 2, Fiance = 4, Bob = Fiance & Admin } One line (max 5 effective) against your 12+. It's also aggravating to have to define code related to an enum outside of that enum's declaration. And strictly speaking, if this were implemented similarly to how C# handles enums, enum Role { NormalUser = 1, Admin = 2, Fiance = 4 }
const Role Bob = Fiance & Admin; Also, what if you also need to convert it the other way? Your example would become num Role { NormalUser, Admin, Fiance, Bob }
extension RoleEx on Role {
int get value {
switch (this) {
case Role.NormalUser:
return 1;
case Role.Admin:
return 2;
case Role.Fiance:
return 4;
case Role.Bob:
return Role.Fiance.value & Role.Admin.value;
}
return null;
}
}
Role getRoleFromValue(int value) {
switch (value) {
case 1:
return Role.NormalUser;
case 2:
return Role.Admin;
case 4:
return Role.Fiance;
case 2 & 4: // equals 6, Very ugly
return Role.Bob;
}
return null;
} The 12+ lines have now become 24+. Not only that, but the parse value for Then imagine you have to do this boilerplate 5 times, 10 times, 20 times, or 100 times in order to support all the defined enums for an API. What seems "not so verbose" gets tedious and annoying very fast. |
@Abion47 I'm not saying it is ideal, and I prefer to have normal enum support in a language as well. But it is not a stopper currently. enum Role { NormalUser, Admin, Fiance, Bob }
extension RoleEx on Role {
int get value {
switch (this) {
case Role.NormalUser:
return 1;
case Role.Admin:
return 2;
case Role.Fiance:
return 4;
case Role.Bob:
return Role.Fiance.value & Role.Admin.value;
}
return null;
}
Role roleByValue(int a) => Role.values.firstWhere((e) => e.value == a, orElse: () => null);
} OR: enum Role { NormalUser, Admin, Fiance, Bob }
extension RoleEx on Role {
static final Map<Role, int> _map = {
Role.NormalUser: 1,
Role.Admin: 2,
Role.Fiance: 4,
Role.Bob: _map[Role.Fiance] & _map[Role.Admin]
};
int get value => _map[this];
Role roleByValue(int a) => _map.entries.firstWhere((e) => e.value == a, orElse: () => null)?.key;
} Not sure what that means: "how would you support the dynamic combination of two enums as in the example above using this approach? (Spoiler: You can't.)" ? |
The point is that it is a stopper for anything much more complicated than a trivial use case. (See below) Role roleByValue(int a) => Role.values.firstWhere((e) => e.value == a, orElse: () => null); This method can't go in the extension since doing so would then require an instance of
Say you had the system where any user had any one of these roles or a combination of them (which is the whole purpose of a bit flag, which itself is the whole point is this thread), and you had a user that was a enum Role {
// base values
NormalUser, // 1
Admin, // 2
Fiance, // 4
// combination values
None, // 0
NormalUserAndAdmin, // 1 | 2 = 3
NormalUserAndFiance, // 1 | 4 = 5
AdminAndFiance, // 2 | 4 = 6 (a.k.a Bob)
NormalUserAndAdminAndFiance, // 1 | 2 | 4 = 7
} (Switching Now we have the 3 base values but also the 5 combination values (including extension RoleEx on Role {
int get value {
switch (this) {
case Role.NormalUser:
return 1;
case Role.Admin:
return 2;
case Role.NormalUserAndAdmin:
return 3;
case Role.Fiance:
return 4;
case Role.NormalUserAndFiance:
return 5;
case Role.AdminAndFiance:
return 6;
case Role.NormalUserAndAdminAndFiance:
return 7;
}
return null;
}
Role roleByValue(int a) => Role.values.firstWhere((e) => e.value == a, orElse: () => null);
} In order to add somewhat of an explicit link between base values and combination values, it requires still more extension methods: extension RoleEx on Role {
...
bool get isNormalUser => this.value & Role.NormalUser.value > 0;
bool get isAdmin => this.value & Role.Admin.value > 0;
bool get isFiance => this.value & Role.Fiance.value > 0;
} This solution is not scalable. Every nth new base value added to On the other hand, native enum bit flag enums would just work: enum Role { NormalUser = 1, Admin = 2, Fiance = 4 }
// From int (options)
Role r = 1 as Role; // cast option
Role r = Role(1); // constructor option
// To int (options)
int i = Role.Admin as int; // cast option
int i = Role.Admin.value; // getter option
// Dynamic combination
Role r = Role.NormalUser | Role.Fiance;
// Is NormalUser (options)
if (r & Role.NormalUser > 0) { // explicit bit-wise AND option
...
}
if (r.contains(Role.NormalUser)) { // helper `contains` method option
...
} No boilerplate, no complicated case statements, everything just works. Yes, workarounds exist, but they are either way too verbose to scale well, break static analysis features, or require bending the language over your knee in order to get them to function properly. At this point, the best workaround would probably be to do away with enums entirely and just make a (there's probably a cleaner way to implement this) abstract class BitFlag<T> {
final String name;
final int value;
const BitFlag(this.name, this.value);
List<T> get values;
T create(int value);
T get sumValue => create(values.cast<BitFlag>().fold(0, (prev, v) => prev | v.value));
bool operator ==(dynamic other) {
if (other is! BitFlag) return false;
return value == other.value;
}
T operator +(Object other) => this | other;
T operator |(Object other) {
if (other is BitFlag) return (create(value + other.value) as BitFlag).trim();
if (other is int) return (create(value + other) as BitFlag).trim();
throw ArgumentError();
}
T operator &(Object other) {
if (other is BitFlag) return (create(value & other.value) as BitFlag).trim();
if (other is int) return (create(value & other) as BitFlag).trim();
throw ArgumentError();
}
T operator ~() => (create(~value) as BitFlag).trim();
bool contains(dynamic other) {
if (other is BitFlag) return this.value & other.value > 0;
if (other is int) return this.value & other > 0;
return false;
}
T trim() {
return create(value & (sumValue as BitFlag).value);
}
@override
int get hashCode => value.hashCode;
@override
String toString() {
if (name != null && name.isNotEmpty) {
return name;
}
final sb = StringBuffer();
for (var v in values) {
if (contains(v)) {
if (sb.isNotEmpty) sb.write(', ');
sb.write((v as BitFlag).name);
}
}
if (sb.isEmpty) return '<Empty>';
return sb.toString();
}
}
class Role extends BitFlag<Role> {
static const NormalUser = Role.value('NormalUser', 1);
static const Admin = Role.value('Admin', 2);
static const Fiance = Role.value('Fiance', 4);
static const _values = [NormalUser, Admin, Fiance];
List<Role> get values => _values;
static const None = Role.value(null, 0);
static final All = NormalUser | Admin | Fiance;
Role(Role role) : super(role.name, role.value);
const Role.value(String name, int value) : super(name, value);
Role create(int value) {
for (var v in values) {
if (v.value == value) {
return v;
}
}
return Role.value(null, value);
}
} void main() {
final bob = Role.Admin + Role.Fiance;
print(bob); // Prints: Admin, Fiance
print(~bob); // Prints: NormalUser
print(bob & Role.Admin); // Prints: Admin
print(bob & Role.NormalUser); // Prints: <Empty>
print(bob | Role.NormalUser); // Prints: NormalUser, Admin, Fiance
print(bob.contains(Role.Admin)); // Prints: true
print(bob.contains(Role.NormalUser)); // Prints: false
print((bob | Role.NormalUser).contains(Role.NormalUser)); // Prints: true
print(Role.All); // Prints: NormalUser, Admin, Fiance
print(~Role.None); // Prints: NormalUser, Admin, Fiance
print(Role.All == ~Role.None); // Prints: true
} You get most if not all the functionality you would expect from a bit flag enum with this class. But again, you lose out on static analysis this way, and it's still more verbose than should be necessary. |
@Abion47 Clearly means you don't need an enum, but class with overridden operators, which your example is exactly demonstrating. |
@slavap As someone who programmed in Java for years, bit flag enums was one of the many features Java didn't support "because no one cared about it" that ultimately made me dump it for C# and never look back. To be honest, looking back at my time as a Java developer, I learned a lot of bad habits from following "best Java practices" and was thoroughly convinced by many common Java idioms that in retrospect made no sense whatsoever. (Seriously, why the hell, after all these years, does Java still not have language-level support for unsigned integral types?)
The whole point of my example was that a class with overridden operators gets most of the way there but still falls short of the convenience, conciseness, and tooling support that language-level support for bit-flag-enabled enums would provide. For example, a language-level bit flag enum could offer seamless interoperability with the Sure, you can do a lot of stuff without specifically needing language-level support for it, but that by itself isn't an argument against adding something. By that logic, we shouldn't implement data classes or algebraic data types either because we can just work around the lack of language-level support with custom classes. And why bother implementing nullable types when its just so easy to check for null? For that matter, why even implement extension methods when you can just write a static utility method? Why bother with generics or static typing when you can just use |
@Abion47 It is all question of priorities. "nullable types" and extension methods definitely are higher priority than "bit enums". |
@slavap Bit-flag enum isn't the feature just by itself. It can easily be implemented as part of the much broader feature of value-typed enums which is the fifth most requested feature of all open issues right now, only behind: 1st place: Data classes 2nd place: Nullable types 3rd place: Make semicolons optional 4th place: Multiple return types Also of note, 7th place: Pattern matching
It's your opinion, though my personal response to that would just be "your loss" since much of that "excessive syntax sugar" leads to incredible increases in productivity once you figure out how to effectively use them. (Kotlin in particular made me seriously consider going back into the Java world for a while.) |
@Abion47 IMO "excessive syntax sugar" leads to incredible unreadable code, especially in case of large development team. You have to enforce more or less unified code style or code review and support will quickly become a nightmare. Look at Golang - simple language without much unnecessary sugar. |
Agreed 💯 💯 💯 on all the points. We really need this feature and having this will help a huge range of use cases in a much efficient way both in terms of code and execution. |
@slavap There is certainly something to be said about keeping things simple and straight-forward, but there's also something to be said about using the right tool for the job. The syntactic sugar tools make life much easier, but just like literally anything else in the programming world, they can be abused. It's not up to us to say whether a tool is good or bad. It's our job as developers to learn not just the right way to use them, but also the wrong ways. There are times that I appreciate Kotlin's expressiveness and conciseness, and there are times I appreciate Go's no-frills to-the-point approach. I rarely want to exist exclusively in one world or the other. Just like the eternal debate between imperative programming, object-oriented programing, and functional programming, why can't the answer simply be "use the best from all of the above"? |
Being able to assign a value against an Enumerated variable is not syntactical sugar at all. |
enum is NOT syntax sugar. enum is known at compile time, but class is only know at run time. |
Being able to assign a value against an Enumerated variable will make code nice and small. Java lacked this with enums, while C# improved on it. It seems Dart is going the Java way. I wouldn't agree with that, I believe any mature language should be able to have a flag type enum type, so instead of 50 lines of extension methods, you'd have 10 lines of enum values. Easy and readable, less error prone, compile time error handling, etc. |
@Abion47 abstract class BitFlag<T> {
final String name;
final int value;
const BitFlag(this.name, this.value);
List<T> get values;
T create(int value);
T get sumValue => create(values.cast<BitFlag>().fold(0, (prev, v) => prev | v.value));
bool operator ==(dynamic other) {
if (other is! BitFlag) return false;
return value == other.value;
}
T operator +(Object other) => this | other;
T operator |(Object other) {
if (other is BitFlag) return (create(value | other.value) as BitFlag).trim();
if (other is int) return (create(value | other) as BitFlag).trim();
throw ArgumentError();
}
T operator &(Object other) {
if (other is BitFlag) return (create(value & other.value) as BitFlag).trim();
if (other is int) return (create(value & other) as BitFlag).trim();
throw ArgumentError();
}
T operator ~() => (create(~value) as BitFlag).trim();
bool contains(dynamic other) {
if (other is BitFlag) return this.value & other.value == other.value;
if (other is int) return this.value & other == other;
return false;
}
T trim() {
return create(value & (sumValue as BitFlag).value);
}
@override
int get hashCode => value.hashCode;
@override
String toString() {
if (name != null && name.isNotEmpty) {
return name;
}
final sb = StringBuffer();
for (var v in values) {
if (contains(v)) {
if (sb.isNotEmpty) sb.write(', ');
sb.write((v as BitFlag).name);
}
}
if (sb.isEmpty) return '<Empty>';
return sb.toString();
}
} The main improvement is that you can now defined a flag that turns other flags on, i.e. you can can define a value like 1 << 2 and other one like (1 << 3) | (1 << 2) and this will set both flags on, also the contains() method is improved to account that change. |
I'm going to close this issue, not because it isn't important (we know it is!), but because it's being tracked over here in the main language design repo: dart-lang/language#158 Please case votes on that issue. We're thinking about this in combination with a larger theme around structured data (see dart-lang/language#546). |
I think that's dart-lang/language#158. |
Thanks Erik, fixed |
Thank you! |
I couldn't find relevant issue which is strange, so if it already exists please pardon me.
Many people are using enums as bit flags (including myself) and in this case scenario current enum implementation is lacking functionality that allows you to assign relevant value to an attribute, eg:
Langauges like c# and java do support this feature, considering fact that dart is taking the best of all modern languages I am finding the above a bit disturbing.
Any idea if this is something that dart will support in near future?
The text was updated successfully, but these errors were encountered: