Defer constructor #2722
Replies: 86 comments
-
Hi, I like this idea, I often find myself needing an Initialize() method which is called from the most derived class. However the problem with this is that it precludes deriving another class. Although I think I would prefer to call the defer constructor bottom up in a similar way to the constructor(or allow the user to specify where the base is called). Is there a better way to handle this problem? David |
Beta Was this translation helpful? Give feedback.
-
Alternatively, allow the base ctor to be explicitly called at any point in the ctor, as in ruby: public abstract class A
{
public abstract string Name { get; }
public SomeThing Value;
public A()
{
if(!dictionary.TryGetValue(Name,out Value)) // can call abstract property Name
dictionary[Name] = Value = new SomeThing(Name);
}
}
public class B : A
{
public override string Name => name;
public readonly string name;
public B(string _name)
{
name = "uniqueString" + _name;
base();
}
} Although I'm not too fond of that either, at least it's a little easier to reason about. |
Beta Was this translation helpful? Give feedback.
-
I don't think it's neccesary to create a new language construct to enable/support blatant violations of CA2214. It would be nice if we could do work before calling the base constructor (as @canton7 illustrated whilst I'm typing this), which IMO would be a vastly more readable approach and require no new syntax nodes. |
Beta Was this translation helpful? Give feedback.
-
Hi, Not sure I understand what makes two stage initialization problematic? I can see how mixing the two stages would be confusing and error prone though. David |
Beta Was this translation helpful? Give feedback.
-
@canton7 I have seen that in some issue and I think that was actually dangerous. It cannot guaranteed that the derived will ever call the And so I completely disagree to any of The point is we just actually need a complete initialization at the constructor. And that just involve allowing base class to access the derived class implementation. Which could be done with abstract or virtual. And so we actually need stack traversal. Top to bottom then return from bottom back to top. Like |
Beta Was this translation helpful? Give feedback.
-
Obviously for backwards compatibility, you will need to implicitly call Your proposal is just splitting the constructor into two parts. Part of the derived class's constructor will be called before part of the base class's constructor (that is after all the entire point of this proposal). This also does "Do work before calling base" -- you've just fudged the problem by calling part of base's constructor "defer" -- but it's still effectively part of the constructor. If you disagree with the semantics of my syntax, then by implication you're disagreeing with your own proposal 🤷♂ I still agree with @yaakov-h in that we shouldn't be adding syntax which encourages people to violate common-sense rules. If you really need this behaviour, you can do it already (although it's a tiny bit ugly): public abstract class A
{
public abstract string Name { get; }
public SomeThing Value;
public A(string _name)
{
Initialize(_name);
if(!dictionary.TryGetValue(Name,out Value)) // can call abstract property Name
dictionary[Name] = Value = new SomeThing(Name);
}
protected abstract void Initialize(string _name);
}
public class B : A
{
public override string Name => name;
private string name;
public B(string _name) : base(_name)
{
}
protected override void Initialize(string _name) => name = "uniqueString" + _name;
} |
Beta Was this translation helpful? Give feedback.
-
This is not. That's why I propose this feature Because the base will always must be call the And the rule is still exactly the same. You can't call abstract or virtual member before the derived class are finished construction. It actually your proposal to ps. I don't understand what are you trying to do with that code? You can't call |
Beta Was this translation helpful? Give feedback.
-
You can call it what you want, but it is still effectively part of the constructor, as it's called as part of the construction process. If you want to make it explicitly not part of the constructor, then do this -- you don't need any special syntax: public abstract class A
{
public abstract string Name { get; }
public SomeThing Value;
protected void Initialize()
{
if(!dictionary.TryGetValue(Name,out Value)) // can call abstract property Name
dictionary[Name] = Value = new SomeThing(Name);
}
}
public class B : A
{
public override string Name => name;
private string name;
public B(string _name)
{
name = "uniqueString" + _name;
Initialize();
}
} |
Beta Was this translation helpful? Give feedback.
-
@canton7 No it's not public abstract class A
{
public abstract string Name { get; }
public SomeThing Value;
protected void Initialize()
{
if(!dictionary.TryGetValue(Name,out Value)) // can call abstract property Name
dictionary[Name] = Value = new SomeThing(Name);
}
}
public class B : A
{
public override string Name => name;
private string name;
public B(string _name)
{
name = "uniqueString" + _name;
Initialize();
}
}
public class C : B
{
public override string Name => "CString" + base.Name;
public C(string _name) : base(_name) { }
}
////
new C("TT"); // Initialize will not call C.Name because it call in constructor of B, not in C and C are not finish construction yet And also your hack is already what I have did before writing this issue. Which is also cannot guarantee that the derived class will ever call |
Beta Was this translation helpful? Give feedback.
-
Did you try your own code? DotNetFiddle.
There are lots of things you can't guarantee. There are many things a programmer can do which will break things. You could set a flag in the base class when it's initialised, and check that before other methods are called, the same as setting a flag when You could also easily write an analyzer which checks whether |
Beta Was this translation helpful? Give feedback.
-
@canton7 while swapping the functionality of the constructor and second phase works, it is even more difficult to see what is going on... |
Beta Was this translation helpful? Give feedback.
-
I'd argue it's about the same level of difficulty. Bear in mind that the OP's suggestion means learning entirely new syntax and remembering how it works (which isn't an easy thing to conceptualise). Syntax like this would be used incredibly rarely, so for most people seeing it they probably won't have seen it before. My suggestion at least uses syntax which already exists and which people are familiar with. |
Beta Was this translation helpful? Give feedback.
-
We have a language and compiler to make something could be guarantee man
If this is already work then it actually should be bug. It means that we actually finished overriding member from the derived class even before it call the constructor of derived class. Which should not happen. This is the actual totally violation of CA2214 |
Beta Was this translation helpful? Give feedback.
-
I think you have a gross misunderstanding of how that code works. C's constructor does not do anything except call B's constructor. You've overridden "Name" to mean |
Beta Was this translation helpful? Give feedback.
-
If this sentence is true then we should not have CA2214 at all from the start. We should always have virtual function be called in the constructor directly. Which is not true It's not about C# constructor |
Beta Was this translation helpful? Give feedback.
-
@HaloFour Sorry if it make you felt bad but I don't intend to insulting. What I said is meant, to think that we should refactoring the constructor to make a static method or factory is the problem |
Beta Was this translation helpful? Give feedback.
-
I think I have seen some people already want At first I don't think that constructor should do anything much too. But after some thought I realize that, constructor is always be the first method everyone think about create any object in the code. If the API provider require that object should be completely initialized by any means, the language should allow them to orchestrate constructor for it. Or the language should deprecated constructor and promote some other means of creation. So every class would have the same single entrypoint I think the design of class like |
Beta Was this translation helpful? Give feedback.
-
@HaloFour And also another problem of both the static create method and factory is, it cannot reflect the constructor of derived class It can't even specify constructor pattern, even if it can (which there was propose for such times as #769 but not much movement) it still restrict that every derived class must implement the same constructor. It then force derived class to not able having new argument it might wanted to. And if you refactor it to be empty constructor and Init method, then it cannot set any readonly field from constructor argument (my defer proposal still don't allow setting a readonly field in defer block, but it still able to set readonly field in normal constructor block) If you thought we should just refactoring everything with pattern then it just like we craft everything with PVC and duct tape. Sure you can make anything this way but it's ugly |
Beta Was this translation helpful? Give feedback.
-
@HaloFour @canton7 Tuple syntax also cause compiler error in older version too. And I don't see anyone in corefx have any problem with that And we also could solve the tuple problem can't be used in older version by include ValueTuple package Then how about we make those code injection as package for older version too is it possible?? |
Beta Was this translation helpful? Give feedback.
-
@Thaina NuGet packages can provide types. They can't cause older compiler versions to learn how to compile things differently. Tuples are different - we use a NuGet package so that you can use tuples with a new compiler but an old framework. The NuGet package doesn't let you use tuples with an old compiler. |
Beta Was this translation helpful? Give feedback.
-
@canton7 I think code analysis and injection or ILManipulation on post build can made into package isn't it?
Then this feature could be the same |
Beta Was this translation helpful? Give feedback.
-
@Thaina Think about it. If you've got a base class compiled with a new compiler and you're compiling a derived class with an old compiler, either the old compiler throws an error, in which case there's no assembly for your post build IL rewriter to rewrite, or the old compiler does not throw an error (and just doesn't call |
Beta Was this translation helpful? Give feedback.
-
You could use an obsolete attribute to tell people to use the nuget package, and then suppress the error manually. In theory. |
Beta Was this translation helpful? Give feedback.
-
@canton7 I think it just that; If we could made a package for magic compiler extension then we need to have using some type in the same assembly as that magic compiler to make it error that it missing the type from some package (maybe attribute to mark that defer block as a function), so people would go grab that package, which also include the attribute and the code injector And if it could not then just make it error in old compiler Anyway I think it would always be the same as how we handle Tuple |
Beta Was this translation helpful? Give feedback.
-
With Tuples, the compiler knew exactly how to deal with the syntax, but was missing the underlying core types. Those types could be supplied by a variety of means including the BCL, user code, or referenced assemblies such as a nuget package. You cannot use a nuget package to fix a binary compatibility breaking change that would be introduced by a new language feature that is inherently incompatible with the calling convention of the old compiler. (Well, you could use |
Beta Was this translation helpful? Give feedback.
-
It's perfectly legal to reference a type in a different assembly, where that type references some other type which you don't know about, I think? That won't cause the old compiler to error. It's worse than that, though. Let's assume that you've put a new attribute on your |
Beta Was this translation helpful? Give feedback.
-
@canton7 If it in new version of BCL then shouldn't it be a new compiler? And if it was old compiler, you shouldn't be able to use BCL, so you need to import the assembly from nuget isn't it? |
Beta Was this translation helpful? Give feedback.
-
IMO this proposal could not emit normal constructors. That's what gets it into trouble with downlevel compilers. Instead, if it were to emit static methods and then treat those methods as constructors you could solve the problem of using the feature from downlevel compilers as well as other languages. // given:
public class Foo {
public Foo(string s) {
Console.WriteLine("Hello");
} defer {
Console.WriteLine("World");
}
}
// the compiler emits:
public class Foo {
private Foo(string s) {
Console.WriteLine("Hello");
}
public static Func<Foo> Create(string s) {
var foo = new Foo(s);
return () => {
Console.WriteLine("World!");
return foo;
};
}
} To updated compilers this could look like a normal constructor: Foo foo = new Foo("bar"); But to downlevel compilers they would have to call the static method directly: // broken into two lines to show detail
Func<Foo> initializer = Foo.Create("bar");
Foo foo = initializer(); I still don't endorse the idea, but this should at least address most of the issues I have with the implementation and avoids the biggest issue which is that by reusing constructors other compilers would silently do the wrong thing. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour that private ctor won't work if there's a derived class (which, after all, is the point of this feature). Once you make the constructor protected, there's nothing to stop a derived class (compiled with a downlevel compiler) from exposing it as public. |
Beta Was this translation helpful? Give feedback.
-
It was the combination of overriding What could possibly go wrong with that? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
To allow initialization able to call abstract member. I think we should have some syntax to allow defer initialization of constructor
The defer block will be called like a stack. Normal constructor would be called from base to derived. But after finished construction it would invoke defer constructor from the derived back to base. And so the each of the base class can call derived class overridden member here
With this feature we would not need any initialization framework and can use only
new
to orchestrate complex initializationBeta Was this translation helpful? Give feedback.
All reactions