Sdk: Allow async constructors (or at least factory constructors)

Created on 7 Apr 2015  路  19Comments  路  Source: dart-lang/sdk

_This issue was originally filed by Jim.J.Si...@gmail.com_


What steps will clearly show the issue / need for enhancement?

  1. Attempt to create a constructor and mark the body as async

What is the current output?
A warning from the analyzer stating that constructors cannot be marked async

What would you like to see instead?
The ability to mark a constructor async. Or at the very least, allow factory constructors to be marked as async. I ran into a situation where I needed to do some post construction logic using an internal method on the constructed object. The method I wanted to use is an asynchronous method and cannot be changed. I ended up using a "static factory method" along the lines of Class.getInstance(), but it seems like this is a situation that would be ideal for a factory constructor.

What version of the product are you using? On what operating system?
1.9.1 on OSX

Please provide any additional information below.

Example:
class Foo {

  //Not allowed :(
  Foo() async {
    await _bar();
  }

  Foo._internal() {
    //normal construction logic
  }
  
  //Also not allowed :(
  factory Foo.factory() async {
    Foo foo = new Foo._internal();
    await foo._bar();
    return foo;
  }

  //Works! But not ideal :(
  static Future<Foo> getInstance() async {
    Foo foo = new Foo._internal();
    await foo._bar();
    return foo;
  }

  Future _bar() async {
    //do some expensive work asynchronously
  }

}

area-language closed-obsolete type-enhancement

Most helpful comment

+1 for async support in factory constructors in order to allow construction of objects with database data and avoid the need to call the async method for complete the object.

All 19 comments

Asynchronous constructors makes little sense - a constructor must return an instance of the class it's a constructor of, and an async function must return a Future.

What you can do is to have static a function that returns a Future, instead of having a factory constructor.

Naming isn't as easy when you can't rely on a "new" in front of the expression. I have so far used "create" as part of the name when no other name seemed reasonable.
I would also usually do the asynchronous work in the static method before creating the object, instead of having an object exist in an uninitialized state.


_Added Area-Language, Triaged labels._

_This comment was originally written by Jim.J.Si...@gmail.com_


@lrn: If you look at the request, you'll see that I mentioned that I ended up using a static method for construction instead of doing the logic in the constructor. While this works, it seemed odd that I had to write a static factory method instead of being able to use a factory constructor. I used the name getInstance, but create would be fine as well.

What really sucks is that I'm now discouraged from using factory constructors in my code for two reasons:

  1. They're not as powerful as the more traditional factory pattern
  2. For the sake of consistency, I don't want to expose two different factory strategies (constructor vs method) to my API's consumer.

I personally don't see any readability or usage issues with something like:

Foo foo = await new Foo();

or even:

new Foo().next((foo){
  //use foo
});

I think this would be a great feature to add to the rich level of async support that Dart already has. If it helps, I'm more than willing to have this thread focus only on factory constructors, since that's what I really wanted to use.

@Andrew: Like any project, I would say that what's next should be based on user feedback. I don't wish to get into a full argument about operators and recursion here unless someone thinks it'll add value to discussing this feature. If you'd like, I'd gladly take this discussion to the mailing list, as I have responses to at least some of your questions and concerns.

_This comment was originally written by Jim.J.Simon...@gmail.com_


Simple case: I have an object with expensive logic for creating its initial state. It could be file I/O, network requests, data crunching, etc. I don't want the user to use the object until that expensive logic if finished, and I don't want to block while it's being executed.

class Foo {

  var expensiveResult;

  //Also not allowed :(
  factory Foo() async {
    Foo foo = new Foo._internal();
    await foo._expensive();
    return foo;
  }

  Foo._internal() {
    //normal construction logic
  }

  Future _expensive() async {
    //do some expensive work asynchronously
    expensiveResult = ...
  }
}

main() {
  Foo foo = await new Foo(); //foo shouldn't be available until its expensive setup is complete
  expect(foo.expensiveResult, isNotNull);
}

_This comment was originally written by @kaendfinger_


Why not just assign a completer in the constructor, and ask the user to do something like instance.onReady

class MyClass {
  Future onReady;

  MyClass() {
    onReady = new Future(() {
      // Do Stuff
    });
  }
}

main() async {
  var instance = new MyClass();
  await instance.onReady;
  print("Ready");
}

Imho, you should never do anything expensive in a constructor. Constructing objects should generally be fast. However, for factory constructors, I do see the benefit. What if you load objects from a database? What then do you do with factory constructors?

_This comment was originally written by Jim.J....@gmail.com_


I can definitely see an argument against doing this for non-factory constructors. What really prompted this enhancement request was the inability to use factory constructors for all use cases that traditional factories can cover. I also agree that, in general, you should never have anything expensive inside a constructor, which is why I ended up using a factory method. Right now, factory constructors support complex initialization logic as long as all of that logic is synchronous.

Your approach of exposing a property is a potential solution, but at that point I could just force the user to call the actual async method itself. The goal was to make initializing the default state as consumer-friendly as possible.

I try to implements Future interface, then factory could return a Future and able to be await.

import 'dart:async';
import 'dart:mirrors';

@proxy
class AsyncFact implements Future {
  factory AsyncFact() {
    return new AsyncFact._internal(new Future.delayed(
        const Duration(seconds: 1), () => '[Expensive Instance]'));
  }

  AsyncFact._internal(o) : _mirror = reflect(o);

  final InstanceMirror _mirror;
  @override
  noSuchMethod(Invocation invocation) => _mirror.delegate(invocation);
}

main() async {
  print(await new AsyncFact());
}

@Ticore: I'm not sure what you are asking for here. Your code already returns a future (an AsyncFact even) which you can "await" on.

You can't mark your factory constructor async because even if it worked, it would return a system Future, not an AsyncFact, so it's not a valid return value for a constructor on AsyncFact.

+1 for async factory constructors. Was my naive approach for constructing a database-backed object for a web-app; didn't work, so everything since then on it has been 'work-around' time for me. Perhaps ideological considerations out-weigh this, but there is justification on the 'batteries included' side of things.

It's not impossible to add to the language, but it will break some of the rules that are generally followed.
One rule is that writing "async" doesn't change the type of the method, it only applies to the body.
If an async constructor factory Foo() async {} returns a Future<Foo>, not a Foo then that's no longer true.

The constructor would have to be a factory constructor anyway. Maybe it should be using async as a prefix:

async factory Foo() => something.something.then((_) => something);

or

async factory Foo() async { await something; return something.else; }

+1 for async support in factory constructors in order to allow construction of objects with database data and avoid the need to call the async method for complete the object.

Is the way in which you create a database dependent objects still with a separate method?

If so I share @zidenis thought on async in factory constructors.

Now that new is optional, it is almost identical to have a factory constructor static method:

class A {
  factory A.a() => ...
}

class B {
  static B b() => ...
}

void main() {
  A.a();
  B.b();
}

... and of course, static methods can have a return type of Future:

import 'dart:async';

class C {
  static Future<C> load(String url) => ... 
}

@lrhn Unless you see this happening (re-open in that case), let's close this.

@matanlurey That's true, with a small caveat: the default constructor name (the best name, sadly) can't be used for static methods. This doesn't work:

import 'dart:async';

class C {
  static Future<C>(String url) => ... 
}

var c = await C('http://google.com');

Is it really going to end here?

I'm amazed that not a lot of people are encouraging this :/

I've come across quite a few moments now where it really could have made everything smoother, like a Settings class with data from your "dynamic" json file for example.

@matanlurey why is this closed?? I started learning dart 2 days ago and I already need async factory constructors. This came super natural. why is this so unwanted?

The same. I use flutter sdk and I need to call async code within constructor:
await _channel.invokeMethod('mymethod')
but I can't.
Solutions with static methods looks ugly.

@rajadain could you reopen?

@konsultaner sorry mate, not up to me, but https://github.com/dart-lang/language/issues/782 looks promising.

I wrote this test code using dart2.7 and flutter 1.20 version, which can be implemented in two ways.

support flutter project.

import 'dart:async';
import 'dart:math';

import 'package:meta/meta.dart';

mixin AsyncInitMixin<T extends Future> implements Future {
  bool _isReady;

  Future<T> _onReady;

  bool get isReady => _isReady ?? false;

  Future<T> get onReady => _onReady ??= _init();

  Future<T> _init() async {
    await init();
    _isReady = true;
    return this as T;
  }

  @override
  Stream<T> asStream() => onReady.asStream();

  @override
  Future<T> catchError(Function onError, {bool Function(Object error) test}) => onReady.catchError(onError, test: test);

  @override
  Future<R> then<R>(FutureOr<R> Function(T value) onValue, {Function onError}) =>
      onReady.then(onValue, onError: onError);

  @override
  Future<T> timeout(Duration timeLimit, {FutureOr Function() onTimeout}) =>
      onReady.timeout(timeLimit, onTimeout: onTimeout);

  @override
  Future<T> whenComplete(FutureOr<void> Function() action) => onReady.whenComplete(action);

  @protected
  Future<void> init();
}

class Test with AsyncInitMixin {
  String data;

  Test({this.data});

  @override
  Future<void> init() async {
    print('init: A ${DateTime.now()}, now data is $data');
    await Future.delayed(Duration(seconds: 1));
    data = '${Random().nextInt(99999999)}';
    print('init: B ${DateTime.now()}, new data is $data');
  }
}

main() async {
  print('test1 await with constructor');
  final Test test1 = await Test(data: 'aaa'); // await constructor
  await test1;
  print('test1 result is ${test1}, and data is ${test1.data}');

  print('--=--=--=--=--=--');

  print('test2 await with onReady');
  final Test test2 = Test(data: 'bbb');
  await test2;
  await test2.onReady;
  print('test2 result is ${test2}, and data is ${test2.data}');

  print('=-=-=-=-=-=-=-=-=');

  print('test1 await with onReady, ensure no reinit');
  await test1.onReady;
  await test1.onReady;
  await test1.onReady;
  print('test1 result is ${test1}, and data is ${test1.data}');
}

The following is the console output log

test1 await with constructor
init: A 2020-09-18 17:45:12.139703, now data is aaa
init: B 2020-09-18 17:45:13.157902, new data is 13464973
test1 result is Instance of 'Test', and data is 13464973
--=--=--=--=--=--
test2 await with onReady
init: A 2020-09-18 17:45:13.158742, now data is bbb
init: B 2020-09-18 17:45:14.168235, new data is 6139666
test2 result is Instance of 'Test', and data is 6139666
=-=-=-=-=-=-=-=-=
test1 await with onReady, ensure no reinit
test1 result is Instance of 'Test', and data is 13464973
Was this page helpful?
0 / 5 - 0 ratings