-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Proposal: Asynchronous Construction #6788
Comments
👍 This wasn't too bad, except for when I had interesting constructors whose behavior now had to be mimicked with the factory methods. i.e. when you had this/base delegating constructors. Having the compiler take care of this for me would be fantastic. Literally wonderful. It would make the process of converting sync to async that much more pleasant, and that much more uniform throughout the language. |
Note: instead of: await using foo = new Foo(); I would think it would be more like: |
@CyrusNajmabadi I thought it would be confusing that |
Since this Foo foo = new Foo(); // compiler error
Task<Foo> foo = new async Foo(); // OK
Foo foo = await new Foo(); // OK |
This would require CLR changes as constructors are invoked through a single IL opcode that is also responsible for allocation. Also, what would make this a particular improvement over a static initializer method? public class Foo {
private Foo() { } // sync initialization here
public static async Task<Foo> Create() {
Foo foo = new Foo();
// async initialization here
return foo;
}
} |
@HaloFour I was considering "regular constructors" but the original proposal wasn't clear about this. Updated.
I think the fact that compiler would handle all this would be really helpful as @CyrusNajmabadi said, in case of inheritance and |
@alrz I agree it would be helpful. But that's still a pretty big change to the CLR. This could be accomplished through a pattern of exposed static methods: public class Foo {
protected Foo() { ... }
public static Task<Foo> Create() {
return Foo.Init(new Foo());
}
protected static async Task<Foo> Init(Foo foo) {
// async initialization here
return foo;
}
}
public class Bar : Foo {
protected Bar() { ... }
public static Task<Bar> Create() {
return Bar.Init(new Bar());
}
protected static async Task<Bar> Init(Bar bar) {
bar = await Foo.Init(bar);
// async initialization here
return bar;
}
} |
I think not. The compiler would generate an |
@alrz Because constructors aren't and cannot be normal methods. They cannot have return types. They can't even return the object being created. They cannot (well, in verified code) be executed directly except through the The only way to avoid that would be to have the compiler translate constructor-like syntax into something like the static async method pattern that I described, but I think it would be very confusing to have |
@HaloFour |
|
Interesting thoughts. I think it could make sense to provide language syntax for this pattern (async modifier on ctor and await new would likely feel most natural) but there are a few hurdles I can think of. I'm on a mobile device now, so will be concise:
More generally, async support is missing in quite a few places besides construction of objects, e.g. properties, indexers, foreach, using. Pushing down async support to the CLR to help with some of these issues can't be ruled out but is rather unlikely I'd guess. |
One more thing wrt bullet two, note that await new isn't backwards compatible either; e.g. await new Task(...) would work today and mean something else than "async construction". Going down the slippery slope of debating syntax prior to a deep semantic debate, "async new" feels wonky because async has been used for declaration sites, and "awaitnew" to be different from "await new" isn't very satisfactory either. There's still "new await" of course :-). |
How about
Nearly all of them are already proposed.
The "async construction" only happens when the constructor is marked with
I have suggested |
And the CLR through verification and code access security. |
@HaloFour If reflection can pass through this, I'm sure compiler would too. |
@alrz The compiler cannot bypass the CLR checks. Direct IL manipulation of the field would be unverifiable. Emitting reflection code to manipulate the field would perform like garbage and that code would be subject to additional security constraints due to code access security. |
As mentioned, it's a much cleaner form. It also makes it much easier to port over existing code to async. It makes the feature much less intrusive with existing language features. I can obviously get the same thing done with a static initializer. But it means a lot more munging around with code. It also means things like base-constructor delegation is far more annoying. I'd like the compiler to just do this for me. As mentioned, I actually just had to deal with this while refactoring the IDE code. Constructors were, by far, the most annoying part of this change. This is also an annoying issue when it comes to an automated refactoring. If the language has async constructors, then it becomes that much easier to provide a "convert this function to async (virally)". Without this feature, you have to do a lot more complicated conversions to make things work. |
I doubt don't for a second that this was a tedious and annoying refactoring, but I don't think that it justifies such a drastic change to the nature of constructors (especially given it couldn't possibly be a constructor.) I'd say that your use case is a better argument for virtual static methods, although I think both are kludgy compared to a proper asynchronous factory pattern. I'd absolutely love a detailed playback as to how this proposal plays out during a language design meeting, though. |
Personally, I think that if you need to asynchronously create an object then you'd be better off with a factory. Is it useful practice to introduce io concerns to an object's constructor in a general enough sense to warrant language support? |
Async methods are fairly drastically different under the covers. But the value is there :)
The "IO" concerns were there before. They were just blocking object construction. I don't se see how that's any better (or how it's any worse to be able to say that the object construction should be async). I also don't see this as necessarily anything that requires CLR changes. This can simply be some sort of transformation that happens at the compiler level. Just as things like async/await, 'yield', and 'dynamic', end up causing massive transformations, but don't end needing any support at the CLR level. |
Regardless, a real proposal would be necessary. but i'm totally behind the need for this. It's always annoying to me when you have language features that are great, until they intersect something else and fall over. "await + try/finally" was an annoying example of this. As is the inability to use things like async/await+yield together. In general, i'd prefer if you didn't continually run into these little annoying restrictions that make the features go from "omg great!" to simply "omg nice!" :) |
Iterators and async methods are still normal CLR methods to the outside observer and their contract and metadata is exactly as they are written in the code. They're called as expected and behave as expected. For all of their internal machinations everything they do can be accomplished without the compiler candy and the consumer would be none-the-wiser. This particular syntax candy requires taking something described as a constructor and to change it into something that is not a constructor and cannot be used as a constructor. If a proposal moved away from that point I'd have no objections. |
@HaloFour I wonder what you have in mind that a regular |
My point is whether or not they should have been in the constructor in the first place. I see constructors that do any "work" as an anti-pattern, and so I see the idea of an async constructor as a feature that enables something you shouldn't do in the first place. But maybe my use-cases just aren't the ones that would benefit from this? |
This is not always true though. 'Dynamic', for example, has a different contract than what's written in code as far the CLR is concerned. For example, a method that has 'dynamic' written as the return type actually has 'object' as its return as far as the CLR is concerned. But it still has vastly different semantics and is treated very differently when this is encountered by the C# compiler (or any other compiler that understands the attributes that C# emits here). |
Similarly, operators and extension methods are special C# entities that use normal CLR methods, but allow use them in a special manner from C#. An operator is something special in C# that becomes just a normal CLR method. So why can't an 'async constructor' be something special in C# that becomes just a normal CLR method? In both cases you have special syntax to declare them in the language, and there are then rules for how you use them. For example, when you make an operator (which just compiles down to a normal CLR method), you still can't call that method directly from C#. Instead, the C# compiler requires that you use the built in syntax (i.e. So why can't this be the case for an async constructor? You have special syntax. It compiles to something special. And you have to consume it in a special way. |
None of those redefine the nature or metadata of an existing CLR construct. They're just methods, perhaps decorated with attributes or just following a specific naming convention. I also tend to agree with @KodrAus that a constructor is the wrong place to do this kind of work. Asynchronous or synchronous, if a type requires heavy lifting to construct that belongs in a factory method, not a constructor. I think that |
I never said async constructors would either.... so i don't see how that is relevant.
They're a C# construct that compiles down to a CLR method. Which, btw, is what i'm saying might be how we do this for async constructors.
Which is also what i'm saying might be the way we do async constructors. It might have an attribute on it as well as a specific namign convention. It would need this (if we use methods) so that the compiler can recognize these when referencing an assembly so that you can use an "async constructor" from another compilation.
Why? And, if that were the case, why do we allow any "heavy lifting" in the first place in a constructor?
I think To me, a factory method is necessary when you may not actually be returning an instance of your return type. i.e.: static BaseType Create(parameters...)
{
... return new Derived1();
... return new Derived2();
} I'm encapsulating that facility to make new things. However, when i know precisely what i want to make, and that thing knows how to make instances of itself, then i don't need a factory. i don't see what "heavy lifting has to do with it either". Why on earth wouldn't i put whatever code made sense in a constructor? Say it does heavy lifting. Ok... so now i make it a factory... How have i helped the situation at all? My code still does heavy lifting... it just does it in a method, and then makes the object, instead of making it in the object itself. How is that code actually better? It's actually more confusing imo because now i have two ways to make the object. I can either use the factory or the constructor. And, even if i make the constructor private, i need to likely know that i shouldn't actually ever use it from within its body (except in the factory method). You're now splitting two things that could be one, and it doesn't seem to buy much. This is why i do not like that my async refactoring forces me into making factory methods. Instead of clear and concise code to begin with, i'm forced to introduce new concepts that i didn't have before. The goal of async/await was to prevent that. It was so that i could have sync code that i could easily make async by sprinkling the right keywords virally outward. imagine if we said "yup! that's how async/await works... except for But, instead, we attempted to make it so that this feature worked well across a fairly large gamut of the language. As it turns out, the amount we got is really darn good. I've found it sufficient for a huge swath of the async work we do. However, there are still pitfalls, and i'd like to remove them to make it far easier and more composable to use this feature. |
@CyrusNajmabadi
As discussion follows:
So finally I vote "yes" for this feature and dismiss myself from discussion. |
We have to decide on the semantics of the feature. :) How basic things like constructor chaining would actually work. Would there be issues inside structs with using async before all fields had been definitely assigned. How does one decide they want something like ValueTask? Can a synchronous constructor chain into an async constructor? etc. etc. Again, the basic feature is incomplete. Hence why the later parts of the discussion are premature (especially as they assumed decisions being made on the CLR that we have not made). |
I don't know if it make sense but because all the operations are async once there's an async constructor, maybe, just maybe it make sense to dictate it based on the kind of type that you use? like if I use a That's just an immediate gut feeling. |
First: Ideally - same as sync constructors work. Just after having to play with another proof-of-concept code (I wouldn't post it here as its implementation on current C# don't illustrate anything beyond proposed syntax) public class Async1
{
// don't propose special syntax other than async keyword
// but REQUIRE first parameter to be "out awaitable" with any name
public async Async1(out ValueTask token, int someVal)
{
// some SYNC code goes here
await something;
// some async code goes here
// no need to assign out awaitable parameter explicitly, it would be done by compiler
}
}
public class Async2 : Async1
{
public async Async2(out ValueTask token, int someVal, string val2)
: base(out token, someVal)
{
// some SYNC code goes here
await token; // it SHOULD be first await in chained async constructor
// some async code goes here
}
}
// Usage
var instance = await new Async1(10); // first "out awaitable" argument is omitted
var instance2 = await new Async2(15, "test");
var instance3 = await new Async1(out ValueTask *, 20);
// first argument type is specified explicitly
// using upcoming "wildcard" out argument syntax to just indicate argument type
// and don't care about its value So, it would be this way:
|
Some reasons why
|
Having the opinion is one thing. Actively claiming that the results of that opinion are in direct opposition to decades of research and aggregate development experience makes one "clueless" (your words, not mine). I can understand (and disagree) with the position that the language shouldn't absolutely force you onto the path of those documented best practices, but the statement that adhering to single-responsibility-principle and separation-of-concerns make for more difficult to test/maintain code is just ludicrous.
Indeed, this conversation is "politically" charged and I've admitted several times that we're certainly not going to convince each other otherwise. It's not worth continuing to follow that line of reasoning. At this point it's like we're arguing climate change; sure, there are some different (and damned vocal) opinions on the subject, despite the vast prevailing consensus. 😉
A contradiction with what? My claim is that C# is as OOP as the majority of other OOP languages where the same best practice is documented. The claim that C# is somehow sufficiently different thus not under the scope of those guidelines is a contradiction.
Which is only a guideline, right? Property accessors are just methods, even moreso than constructors (the latter having much more rigid rules per the CLR). Why shouldn't the language enable and encourage developers to put all kinds of logic in them? A million years ago the way you'd print a report in Crystal Reports was through writing to a property: CrystalReport1.Action = crRunReport ' 1 The accessor would them block until the report was printed. If it's good enough for Crystal Reports surely it's good enough for C#? 😉
But what if asynchronous constructors can't attain parity with synchronous constructors? Then you'd have something that looks and smells like a constructor, but can't be a constructor. That whole goal of being able to seamlessly refactor a synchronous constructor into an asynchronous constructor becomes impossible without further changes to the class. Is it really worth it then? Again, this is why I bring up implementation. Ignoring whether or not this feature is a good idea from a design point of view, I think that there are still concerns which make it a bad idea from a functionality point of view. Resolving those issues requires either explicitly deciding to ignore them (a valid choice) or increasing the scope of work. All of a sudden that simple low-hanging fruit moves a few branches up that tree. Is the tree still worth climbing? Even if the answer is "yes" these are concerns that need to be addressed. I'm not satisfied with the answer that all of that can be deferred to some nebulous time in the future. We have the bandwidth and brainpower to at least go through some rough drafts and come up with action items.
It's funny that you mention "pits". Do you think that asynchronous constructors represent a pit of success? Personally I don't, both from a design guideline and feature parity point of view. But we all know what they say about opinions: mine are sweet and fragrant. 😀 |
@HaloFour Yup like you said we will never convince one another. 😉
Yeah, I meant that in our view we think that because async construction require "some work" before the object is initialized it isn't considered "the kind of work" they write about in guidelines whereas in your view it's a slippery slope, reasonably so I'd say but I still disagree. 😆
You're right, I don't have a good argument against it but I can just say that conceptually they describe two different things, hence, they are expressed in the language as two different concepts.
Things change and maybe they are about to change again... 😄
Once we will get into actual implementation details and there will be some blockers that will prevent it from doing it right I'd accept the judgment!
Still late and too far to tell, I have my doubts but I'm willing to have a discussion about it and see where we can go with it than doing nothing and wonder about it! that's where I stand, haha.. |
As I read all this (which is very interesting) I come to 2 conclusions: *If constructor become async, then properties should definitely be async.
I think the person who "owns" c# should take this call or should decide on taking a developers survey to see if this feature is actually wanted a lot. (I think more than half of c# developers are barely even using async yet) |
I think that this is a bad assumption. :) |
I think not doing any heavy lifting (let's say it's out of process work, since this has async benefit) in the constructor is a good practice because it creates a dependency on this work and on the result. Clearly if a class needs to prepare a lot of heavy lifting in the he constructor this class has 2 responsibilities now: 'prepare himself to be able to do his job' and 'do his job'. I'd say rather inject the result of the preparation into the class his constructor. Another solution would be to use first time execution (heavy lifting on first execution, also called lazy execution). like an initialize function, but it is only executed when you need it, and cache it after first execution. With all the developments on dependency injection. I think this problem can be fixed my making async resolve functionality in the di container, rather than implementing it in the language itself. container.register(async (c) => {
var lifter = c.resolve<Lifter>();
var result = await lifter.DoSomeHeavyLifting();
return new ClassInNeed(result);
} Also doing heavy lifting inside a constructor makes unit testing slow or at least harder mockable. Also a third party developer will think your constructor code is fast (same as him thinking properties are fast). Giving it extra capabilities for async constructors will only make it harder for 3rd party developers to learn & use your code properly. I think time and energy is better spend in other language features. |
In your camp it does two things in our camp it does only one thing because my camp we don't think that the constructor is doing any work, it's all part of the object initialization.
This has nothing to do with this topic.
Not everyone serve their dependencies through a container.
This isn't true, the fact that you said that it will make testing slow just discredit you.
No, they wouldn't because they would have to call the constructor with async and it kinda implies that the operation won't finish immediately.
I think it's time to let the design team decide. |
Ofcourse it makes your testing 'slow'. If you make your heavy lifting in your constructor. and you write n * amountOfMethods unit tests for this class. the constructor including heavy lifting is called n*amountOfMethods times. instead you should only inject the end result of the heavy lifting in the unit test so the tests stays fast + you only test the logic of the method and not the heavy lifting included. The heavy lifting should be in its own method which can be tested separately. |
@joelharkes I know that async is slower than synchronous calls due to all the magic behind that make this possible and I understand the point of separation of concerns or decoupling but I can't see why async by itself would be the source that drag your tests and make them slower. Maybe I'm missing your point or misunderstand you, can you give me an example? |
No, your right, async constructors it self are not the bad, but rather what it provides. It would only provide a way to go against common practice and use your constructor for heavy lifting. I do agree it's a nice feature but I also think it should only be used if you want to make something work quick and dirty. This is why I stand by my view that other features should have more prio. |
I don't think that it applies only for when you want to get the job done and do things quick and dirty, people still use iterations over LINQ and interfaces over delegates when it make sense and it's right tool for the job. I can certainly see the use of abuse here but I really don't think the language should educate you about usage, much like the English language by itself doesn't educate me to choose the words I use. Yes I believe the language need to push developers into the pit of success if possible but not always, sometimes the developer need to be responsible to understand what he does. |
Your perspective of the subject is certainly valid, but my confidence with async has only grown with experience. I think async/await is a stellar implementation on every design level and it never gets old using it where idiomatic. When it comes right down to it for me, as a consumer of the language, I'm modeling concepts. And when the conceptual object that I'm modeling cannot possibly come into existence in any sort of valid state without waiting for an async call, it's ugly to not be able to express that via With the
This doesn't even get into the fact that I can't mark fields All I want is an expressive model. No one reading my code will be confused and think that |
"Asynchronous constructors" won't fix this problem. Any field you attempt to set after an
The syntax can't be |
Initonly fields are already neutered once you move beyond the C# language into CLR reflection. My only expectation would be that it would just work and be enforced as a C# language feature. Nothing more. I don't actually care if they are
This is new to me. Why? |
Reflection requires full trust. Making
The existing rule is that |
@HaloFour That's a good argument for CLR async initonly support. I don't see how it's that different from existing behavior. There is a period of time where the fields can be assigned from constructor method bodies and the instance cannot be observed except inside the constructor, and then the construction is over and further assignments are not possible and the instance can be observed by the outside world.
Of course, but that is fringe to non-existent and shouldn't hold this proposal back. If instance methods or extension methods exist which make |
I have 2 mind for this At first I think I would go against this idea. Because constructor should always be synchronous in memory. And if you need to load something it should be factory But on the other hand. Factory is so hard to extend. Generic constraint on So to have sophisticate async factory we need to implement so manything. async constructor would be fastest way to solve this problem |
Here is example of how public class A
{
public readonly int X;
public async A(int x)
{
await Task.Delay(0).ConfigureAwait(false);
X = x;
}
}
public class B : A
{
public readonly int Y;
public async B(int x, int y) : base(x)
{
await Task.Delay(10).ConfigureAwait(false);
Y = y;
}
} which could be translated into: public class A
{
public readonly int X;
protected A(int x)
{
X = x;
}
[AsyncConstructor]
public static async Task<A> <A>__CreateAsync(int x)
{
await Task.Delay(0).ConfigureAwait(false);
return new A(x);
}
}
public class B : A
{
public readonly int Y;
protected B(int x, int y) : base(x)
{
Y = y;
}
[AsyncConstructor]
public static async Task<B> <B>__CreateAsync(int x, int y)
{
await Task.Delay(0).ConfigureAwait(false);
var <x>__Field = x; // instead of setting field we are setting local variable. We can further use it, there is no difference for the code below
await Task.Delay(10).ConfigureAwait(false);
var <y>__Field = y;
return new B(<x>__Field, <y>__Field);
}
} I am using fields because properties may have some additional logic and probably cannot be replaced with local variables. More investigations are required here, but that looks good for proof-of-concept |
That method breaks down when |
Now I see why we should not have async constructor What happen if both This cannot translate into factory automatically. And how could runtime handle half finish object? |
@Thaina I can't imagine any visible to client state when you have an access to partialy-inited object. We are creating empty object of type |
@Pzixel You are right if we have CLR support it directly. I mean we can't workaround with factory only by compiler |
@alrz Can you port this issue to the CSharpLang repo? |
@MgSam Yes. I'm on it. |
Moved to dotnet/csharplang#419 |
Inspired by an article on async initialization:
The point is, to be able to write:
To not alter the existing constructor semantics, the compiler would generate an
async
method corresponding to theasync
constructor and use it to initialize the object.For local variable declaration we use
This should be integrated with "async disposables" #114 and #5881 so that when it's out of scope, the disposal should be taken care of.
It's a compiler error if one called an
async
constructor withoutawait
. in order to get the involvingTask
you may use the following syntax:For initializing class fields we do as usual:
This makes the whole class
async
because you cannot instantiate this class with any of regular (or, obviously,async
) constructors withoutawait new
ornew async
anymore.Also, for async disposable types, the #111 should be taken into account.
async class
can be used also for classes that useawait
on methods in field initialization:Then an
await new
ornew async
shall be needed in order to instantiate the type.The text was updated successfully, but these errors were encountered: