Reputation: 1857
I want to be able to construct an object from inside a generic function. I tried the following:
abstract class Interface
{
Interface.func(int x);
}
class Test implements Interface
{
Test.func(int x){}
}
T make<T extends Interface>(int x)
{
// the next line doesn't work
return T.func(x);
}
However, this doesn't work. And I get the following error message: The method 'func' isn't defined for the class 'Type'
.
Note: I cannot use mirrors because I'm using dart with flutter.
Upvotes: 44
Views: 20237
Reputation: 89926
You cannot call constructors or static
methods on the type arguments to generics.
Dart generics are not like templates in C++, where the concrete type argument is substituted into the template first to generate a concrete class, and that concrete class then is compiled. The C++ template approach results in separate compiled versions of the template for each combination of type parameters used by the program. (For example, std::vector<int>
, std::vector<double>
, std::vector<std::string>
, etc. are all compiled separately.) This results in longer compilation times and larger compiled program sizes. Prior to C++20's constraints and concepts, this meant that C++'s template type arguments essentially used duck typing, and that typically led to confusing error messages if type substitution failed.
In contrast, Dart generics are compiled once. Consequently, wherever a Dart generic class or function uses a type argument, the static analyzer must verify that that usage is known to be valid before any concrete type is substituted. For example:
void printSquare<T>(T x) => print(x * x); // ERROR
will not compile in Dart because operator *
does not exist for every possible value for T
. In such a case, T
would need to be constrained to guarantee that its interface provides operator *
:
void printSquare<T extends num>)(T x) => print(x * x); // OK
However, in Dart, constructors and static
methods are not part of the class's interface. They are not inherited; if a base class has a constructor with a particular signature, there is no guarantee that a derived class provides the same constructor. Therefore there is no way to constrain a type argument to guarantee that it has a particular constructor or static
method. In other words:
final _registry = <Object?>[];
void registerNewObject<T>() {
var object = T(); // ERROR
_registry.add(object);
}
cannot work because there is no way to guarantee that every possible T
has an unnamed constructor that can take no arguments.
Instead, you should have callers pass a callback that invokes the constructor or static
method that you want. Using a callback is additionally more flexible since callers would not be tied to a specific method name with a specific signature. For example:
final _registry = <Object?>[];
void registerNewObject<T>(T Function() newObject) {
var object = newObject();
_registry.add(object);
}
class Foo {}
class Bar {
final String name;
Bar(this.name);
Bar.anonymous() : name = '';
}
void main() {
registerNewObject(Foo.new);
registerNewObject(Bar.anonymous);
registerNewObject(() => Bar('Baz'));
}
Upvotes: 1
Reputation: 803
A variation of Günter Zöchbauer's excellent answer and using the new constructor tearoff feature in Dart 2.15 could be to add an extra parameter to the generic function to provide the constructor (using Test.new
for a default constructor, if required).
T make2<T extends Interface>(int x, T Function(int) constructor)
{
return constructor(x);
}
make2<Test>(5, Test.func);
// Default constructor
make2<Test>(5, Test.new);
The T Function(int) constructor
parameter is a Function which takes an int
argument and returns Type T
.
You could have done this without constructor tear-offs too, but (for me at least) this is just a little too much to parse.
make2<Test>(5, (int x) => Test.func(x));
This also has the useful attribute that if a developer copies and pastes the function without changing the parameter, they should be alerted by static analysis (since AnotherTest.func
doesn't return Test
), rather than only finding out at runtime when the factory list is compared to the types.
// ERROR: The argument type 'AnotherTest Function(int)' can't be
// assigned to the parameter type 'Test Function(int)'
make2<Test>(5, AnotherTest.func);
Upvotes: 13
Reputation: 21
So as of late 2022 the cleanest way I've found to do this is to use an intermediate class that takes a tear away constructor as a parameter and derives the type from it.
Example for Generic Events:
This is something I made for inter process communication that only allowed passing strings.
The events are typed, but automatically converted into a string on send and parsed back into objects on receive.
The Dream
var OnResized = new MessageEvent<Sized>("resized");
The Reality
var OnResized = new MessageEvent("resized", Size.fromJson);
It's not as clean as I would like, but ultimately it's only an extra 8 characters.
Usage
OnResized+=SizeChanged;
void onSizeChanged(Size size) {
}
OnResized.Invoke(new Size(399,400));
This is where storing the factory pays off. The users of the class are completely oblivious to the inner workings and don't have to worry about typing at all.
Implementation
typedef T CreateFromJson<T extends ISerializable>(Map<String, dynamic> json);
typedef void MessageHandler<T>(T args);
class MessageEvent<T extends ISerializable> {
String Name;
CreateFromJson<T> ReseultParser;
void _notify(String data) {
var result = ReseultParser(json.decode(data));
for (var callback in _callbacks) {
callback.call(result);
}
}
void Invoke(T data) {
_sendMessage(Name, json.encode(data.toJson()));
}
MessageEvent(this.Name, this.ReseultParser) {
_addEventListener(Name, this);
}
final List<MessageHandler<T>> _callbacks = <MessageHandler<T>>[];
void Add(MessageHandler<T> callback) {
_callbacks.add(callback);
}
MessageEvent<T> operator +(MessageHandler<T> callback) {
Add(callback);
return this;
}
static void OnMessage(MessageParams message, dynamic other) {
_listeners[message.type]?._notify(message.params);
}
static _sendMessage(String type, String args) {
SendMessageProtocall?.call(MessageParams(type: type, params: args));
}
static SendProtocall? SendMessageProtocall;
static final _listeners = <String, MessageEvent>{};
static void _addEventListener(String type, MessageEvent event) {
_listeners[type] = event;
}
}
Upvotes: 0
Reputation: 549
I haven't seen any good (complete/concrete/non-esoteric!) workaround examples, so here's mine:
Use as follows:
void main() {
final user = Model.fromJson<User>({
"id": 1,
"username": "bobuser",
"name": "Bob",
"email": "[email protected]",
});
print(user.runtimeType);
print(user is User); // Unnecessary type check; the result is always 'true'.
print(user.toJson());
}
dart lib/main.dart
User
true
{id: 1, username: bobuser, name: Bob, email: [email protected]}
abstract class Model {
/// It's really a shame that in 2022 you can't do something like this:
// factory Model.fromJson<T extends Model>(Map<String, dynamic> json) {
// return T.fromJson(json);
// }
/// Or even declare an abstract factory that must be implemented:
// factory Model.fromJson(Map<String, dynamic> json);
// Not DRY, but this works.
static T fromJson<T extends Model>(Map<String, dynamic> json) {
switch (T) {
case User:
/// Why the heck without `as T`, does Dart complain:
/// "A value of type 'User' can't be returned from the method 'fromJson' because it has a return type of 'T'."
/// when clearly `User extends Model` and `T extends Model`?
return User.fromJson(json) as T;
case Todo:
return Todo.fromJson(json) as T;
case Post:
return Post.fromJson(json) as T;
default:
throw UnimplementedError();
}
}
Map<String, dynamic> toJson();
}
class User implements Model {
User({
required this.id,
required this.username,
required this.name,
required this.email,
});
final int id;
final String username;
final String name;
final String email;
factory User.fromJson(Map<String, dynamic> json) => User(
id: json["id"],
username: json["username"],
name: json["name"],
email: json["email"],
);
@override
Map<String, dynamic> toJson() => {
"id": id,
"username": username,
"name": name,
"email": email,
};
}
class Todo implements Model {
Todo({
required this.id,
required this.userId,
required this.title,
required this.completed,
});
final int id;
final int userId;
final String title;
final bool completed;
factory Todo.fromJson(Map<String, dynamic> json) => Todo(
id: json["id"],
userId: json["userId"],
title: json["title"],
completed: json["completed"],
);
@override
Map<String, dynamic> toJson() => {
"id": id,
"userId": userId,
"title": title,
"completed": completed,
};
}
class Post implements Model {
Post({
required this.id,
required this.userId,
required this.title,
required this.body,
});
final int id;
final int userId;
final String title;
final String body;
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json["id"],
userId: json["userId"],
title: json["title"],
body: json["body"],
);
@override
Map<String, dynamic> toJson() => {
"id": id,
"userId": userId,
"title": title,
"body": body,
};
}
Upvotes: 11
Reputation: 657128
Dart does not support instantiating from a generic type parameter. It doesn't matter if you want to use a named or default constructor (T()
also does not work).
There is probably a way to do that on the server, where dart:mirrors
(reflection) is available (not tried myself yet), but not in Flutter or the browser.
You would need to maintain a map of types to factory functions
void main() async {
final double abc = 1.4;
int x = abc.toInt();
print(int.tryParse(abc.toString().split('.')[1]));
// int y = abc - x;
final t = make<Test>(5);
print(t);
}
abstract class Interface {
Interface.func(int x);
}
class Test implements Interface {
Test.func(int x) {}
}
/// Add factory functions for every Type and every constructor you want to make available to `make`
final factories = <Type, Function>{Test: (int x) => Test.func(x)};
T make<T extends Interface>(int x) {
return factories[T](x);
}
Upvotes: 39