dotnet / csharplang

The official repo for the design of the C# programming language
11.54k stars 1.03k forks source link

Champion "Target-typed `new` expression" (VS 16.8, .NET 5) #100

Open gafter opened 7 years ago

gafter commented 7 years ago

Summary: Allow Point p = new (x, y);

Shipped in preview in 16.7p1.


See also https://github.com/dotnet/roslyn/issues/35

Test plan: https://github.com/dotnet/roslyn/issues/28489

alrz commented 6 years ago

@HaloFour the issue is where none of initializer elements have a type, like a lambda or a method group.

jnm2 commented 6 years ago

@HaloFour That is not target-typing though. What @alrz said; see also new[] { }, new[0] and new { null }.

jnm2 commented 6 years ago

@alrz I'm on the fence about #414. I'm not asking to drop the new keyword. I would just like to see the type inferred from the target regardless of whether we're talking about new(), new { } or new[]. I know the latter results in a different type, but I want the syntactic symmetry. Why wouldn't we go for that?

alrz commented 6 years ago

@jnm2

If I understand correctly, I think your proposal is similar to https://github.com/dotnet/csharplang/issues/877 in a sense that whenever we fail to infer a "best type" we should consider the target-type for the whole expression. So both of these could be valid:

class C1 {
    void M(C2 a, C3 b, bool c) {
      C1 x = c ? a : b;
      C1[] z = new[] { a, b };
    }
}
class C2 : C1 {}
class C3 : C1 {}

That is different from new() because here we look at target-type constructors to infer the type, while in those cases we should rely on a failure in best-type algorithm. but I do think that makes sense to have both.

asyncawaitmvvm commented 6 years ago

I scream for this feature every time I have to instantiate a Dictionary<,>.

IanKemp commented 6 years ago

@asyncawaitmvvm Amen, brother!

alrz commented 6 years ago

https://github.com/dotnet/csharplang/issues/877 also helps with new(),

Size screenSize = isJurassic ? new(800, 600) : new(3840, 2160);

Otherwise, you just cannot use new() as both operands of a ternary.

AustinBryan commented 6 years ago

@Thaina But you can't use var in certain situations. Here's a few:

// Format: List<(Dictionary<string book name, string author>, int copies)>

// Before: 
public List<(Dictionary<string, string>, int)> MyBooks => myBooks ?? myBooks = 
   new List<(Dictionary<string, string>, int)>();
// After:
public List<(Dictionary<string, string>, int)> MyBooks => myBooks ?? myBooks = new();

// Before: 
PrintBooks(new List<(Dictionary<string, string>, int)>());
// After:
PrintBooks(new);

// Before:
public List<(Dictionary<string, string>, int)> MyBooks = new List<(Dictionary<string, string>, int)>();
// After:
public List<(Dictionary<string, string>, int)> MyBooks = new();
Thaina commented 6 years ago

@AustinBryan There was a proposal to let we use var on class level. It was argued that it a lot harder to implement. But I think it worth to have consistent syntax

HaloFour commented 6 years ago

@Thaina

There was a proposal to let we use var on class level. It was argued that it a lot harder to implement. But I think it worth to have consistent syntax

You are welcome to your opinion, although said proposals are of the very few that seem to get immediately shot down and closed by the language team, e.g. https://github.com/dotnet/csharplang/issues/1484

AustinBryan commented 6 years ago

@Thaina I doubt that will ever happen. It seems to be much harder to do, while new would just be yet another compile trick that it can do before runtime. And even still, that would only clean up the last case. var could only be used to declare variables, but the first two ways aren't declaring variables.

// var: 
public var MyBooks => myBooks ?? myBooks = new List<(Dictionary<string, string>, int)>();
// new:
public List<(Dictionary<string, string>, int)> MyBooks => myBooks ?? myBooks = new();

That would be a personal preference, meanwhile, this:

// Before: 
PrintBooks(new List<(Dictionary<string, string>, int)>());
// Var:
PrintBooks(var);
// New:
PrintBooks(new());
PrintBooks(new);

Doesn't even remotely make sense, so new() will always be needed in that case, and it should be there to mirror PrintBooks(default) and make the language more consistent.

AustinBryan commented 6 years ago

If the object doesn't have parameters, should? new without () work, instead of new()? I would prefer that because it would better match default.

alrz commented 6 years ago

If the object doesn't have parameters, should? new without () work, instead of new()? I would prefer that because it would better match default.

It would also match VB where invocation parentheses are optional where there's no arguments.

jnm2 commented 6 years ago

If new is equivalent to new(), it implies that new { A = 1 } is equivalent to new() { A = 1 }.

Ghost4Man commented 6 years ago

default doesn't call anything, unlike new(). Parentheses often imply that a piece of code will be executed.

jcouv commented 6 years ago

@AustinBryan LDM is holding back on "naked" new (without ()) for now. That will be in the 2018-06-25 notes (sorry, not yet published).

HaloFour commented 6 years ago

Naked new I think would create ambiguity. The following is already legal:

public class Foo {
    public int A { get; set; }
    public string B { get; set; }
}

public class C {
    public void M() {
        M(new { A = 123, B = "abc" });
    }

    public int M(Foo o) => 1;
    public int M(object o) => 2;
}

If new would then be considered to create Foo instead of an anonymous type that would result in the wrong overload being selected.

IanKemp commented 6 years ago

This feature will save me so much typing that I don't care one iota about having to insert an extra two parentheses. And I definitely agree that (regardless of the possible ambiguity) the parentheses make it clear that this is an invocation rather than a simple assignment, as noted by @Ghost4Man .

Logerfo commented 6 years ago

M(new); is not already legal, I think it can be considered for this feature. M(new { A = 2 }); is already legal and should keep working as it is: an anonymous object. I understand if the team decides not to make the former legal for symmetry reasons

svick commented 6 years ago

From the design meeting notes:

Nullable value types

Without special treatment, they would only allow the constructors of nullable itself. Not very useful. Should they instead drive constructors of the underlying type?

S? s = new (){}

Conclusion

Yes

Does this mean that S? s = new(); and S? s = new S?(); would produce different results? That sounds confusing.

jnm2 commented 6 years ago

I actually prefer that inconsistency.

NoofSaeidh commented 6 years ago

We already have "var" that enough for variables in methods. Isn't it better to add "var" into fields, properties?

public var IntValue => 5; public var ClassValue => ClassValue.Default; private readonly _structValue = new StructValue();

IanKemp commented 6 years ago

@NoofSaeidh Wrong issue? This is about implicitly-typed new, not var (for the latter see #1484).

ufcpp commented 6 years ago

@NoofSaeidh

void M(Dictionary<string, int> x) { }

M(new { { "a", 1 }, { "b", 2 }  });
quinmars commented 6 years ago

@ufcpp I hope the empty parenthesis are obligatory here.

M(new () { { "a", 1 }, { "b", 2 }  });
jnm2 commented 6 years ago

I second @quinmars (and prefer new() with no space, like other method calls).

HaloFour commented 6 years ago

@quinmars @jnm2

Per LDM notes the "naked" new is being held off.

https://github.com/dotnet/csharplang/issues/100#issuecomment-402862850

Given the ambiguity with anonymous types I doubt that will be reconsidered.

alrz commented 6 years ago

S? s = new (){}

Probably not the best example. With special treatment of nullable value types, that would never work - at least, not until https://github.com/dotnet/csharplang/issues/99 lands) because we filter out struct default constructors.

@svick

Considering that, assuming S have another constructor S(int), the following:

S? s = new(43);

would call the S(int), otherwise, you'll need to wrap yet another new to account for the nullable type.

S? s = new(new(43));

So here we reduce the noise by targeting the underlying type in case of a nullable type.

iam3yal commented 6 years ago

@SymmetryKeeper-V The same argument can be made about var and other features, people can abuse the feature proposed here but they can also use it to improve the readability of their code.

I don't think that you can call the code you posted an alternative, it's expensive to maintain, inferior if you care about readability and I'm not even speaking about allocations and performance.

Barsonax commented 6 years ago

Saw the target typed new expressions in the .NET blog article here. In the example it seems that 'new' itself is redundant:

Point[] ps = { new (1, 4), new (3,-2), new (9, 5) }; // all Points

Why not like this:

Point[] ps = { (1, 4), (3,-2), (9, 5) }; // all Points
YairHalberstadt commented 6 years ago

@Barsonax Because that would mean a new Tuple. This is confusing, and furthermore ambiguous if Tuple implicitly converts to Point.

Besides new is a useful indicator that a new object is being created (possibly on the heap). Whilst less relevant for structs, that's extremely important information for classes. Is 3 characters really too long?

Barsonax commented 6 years ago

Because that would mean a new Tuple. This is confusing, and furthermore ambiguous if Tuple implicitly converts to Point.

The type is known to be Point because the variable is of that type. Nothing is ambiguous about that. Even if there is a implicit conversion from Tuple to Point it should use the constructor. If you think its confusing its because implicit conversions themselves can be confusing (there many ways to abuse those). Besides even with a implicit conversion from Tuple to Point it won't convert the array from a Tuple array to a Point array. It would simply result in a compile error.

Besides new is a useful indicator that a new object is being created (possibly on the heap). Whilst less relevant for structs, that's extremely important information for classes. Is 3 characters really too long?

new in itself has nothing to do with the heap as that has to do with is it a struct or a class. If you write (1,4) you are actually writing new ValueTuple<int,int>(1,4) so there already cases where new is omitted. Also because of the space its actually 4 character saved per element and as you can see in the example its significantly shorter.

If 'hidden' heap allocation is a concern consider enabling this syntax only for structs. Most of the use cases would be structs anyway.

YairHalberstadt commented 6 years ago

On the contrary, if there is an implicit conversion from tuple to point this is currently a valid expression, so you will be breaking existing code.

So the only option is to specify that when an implicit conversion exists, the creation of a tuple wins. In which case it's impossible to tell locally the type of the object being created. You have to know whether an implicit conversion from tuple exists.

Barsonax commented 6 years ago

On the contrary, if there is an implicit conversion from tuple to point this is currently a valid expression, so you will be breaking existing code.

Foo[] fooarray = new[] { (1, 2) };

Won't compile even if Foo can be implicitly converted from ValueTuple<int,int> to Foo. It can convert a single element but not a array of these elements.

YairHalberstadt commented 6 years ago

https://sharplab.io/#v2:EYLgZgpghgLgrgJwgZwLQDk4BstWFiAJQkiQDsBjCAFQE8AHFAGhhAEssmATEAagB8AAgAYABIICMAbgCwAKEEBmcQCZRABQD2bMjFEBveaONLxEgGyi2AW3pY2FNns2MEsTQg3bdACh869KCYrXVFgAEpReDsISIBeAD5RMggAdy8An3DZOWNjIzzTQQAWUSgEN1osguNDXLyGgDdysorROOS0jN0AbQBdfR8JJhVw4J9FJlQAVnCAXxyGuflluSA==

jnm2 commented 6 years ago

Requiring the new keyword to represent "executing a constructor" feels more right to me and easier to understand.

Barsonax commented 6 years ago

https://sharplab.io/#v2:EYLgZgpghgLgrgJwgZwLQDk4BstWFiAJQkiQDsBjCAFQE8AHFAGhhAEssmATEAagB8AAgAYABIICMAbgCwAKEEBmcQCZRABQD2bMjFEBveaONLxEgGyi2AW3pY2FNns2MEsTQg3bdACh869KCYrXVFgAEpReDsISIBeAD5RMggAdy8An3DZOWNjIzzTQQAWUSgEN1osguNDXLyGgDdysorROOS0jN0AbQBdfR8JJhVw4J9FJlQAVnCAXxyGuflluSA==

Seems it doesn't handle it consistently atm:

    public class Point
    {
        public static implicit operator Point((int a, int b) tuple) => new Point();

        public void array()
        {
            var foo1 = new Point[] { (1, 2), (3, -5) }; //Compiles
            Point[] foo2 = { (1, 2), (3, -5) }; //Compiles
            Point[] foo3 = new[] { (1, 2), (3, -5) }; //Error
        }
    }

So actually even today there ways to abuse the system and omit the new.

YairHalberstadt commented 6 years ago

@Barsonax

Of course you could allow new [] which is not ambiguous, but not allow new Point[] which is. However that's even more confusing, and of course doesn't help with any other collection types.

YairHalberstadt commented 6 years ago

@Barsonax There's no abusing the system going on. There's an implicit operator being used, and you are creating a new ValueTuple, which is converted to a new point.

Barsonax commented 6 years ago

I makes more sense if it would pick the constructor of Point instead of going through the implicit conversion:

    public class Point
    {
        public static implicit operator Point((int a, int b) tuple) => new Point(tuple.a, tuple.b);

        public Point(int p1, int p2)
        {

        }

        public void array()
        {
            Point[] foo2 = { (1, 2), (3, -5) }; //Works but picks the implicit conversion..
        }
    }

That's a hidden performance drain which is possible in todays code.

YairHalberstadt commented 6 years ago

@Barsonax But you can't change the behaviour of currently working code, so it's irrelevant what the ideal behaviour should have been

Barsonax commented 6 years ago

One could doubt if this would break anything in a real application though. Well unless you are trying to find a situation on purpose.

YairHalberstadt commented 6 years ago

@Barsonax Perhaps. But to take the risk, the benefits must be great enough that this becomes worth it. And I personally think that the word new is a good indicator that a constructor is being used. As such I personally think this is not only not worth it, but actively damaging.

It also means that every single tuple declaration must be analyzed by the compiler to see if it's actually a new expression, and vice versa. This makes the compiler significantly more complex, as this can't be decided in the parsing stage. It would have to be disambiguated afterwards.

Barsonax commented 6 years ago

Well it might not be too bad I just hope the JIT recognizes this correctly and optimizes it. If it manages to optimize the method call and the tuple creation away and simply directly calls the constructor it should result in the exact same machine code. Usually the implicit converter body is very small and thus candidate to inlining which should enable this.

For complex cases it probably cannot optimize this but I suspect these to be very rare.

EDIT: well turns out it does not optimize this so just have to be wary when I see these kind of constructs in code.

PathogenDavid commented 5 years ago

It's basically the same, but you can avoid the pointer usage with Unsafe.As<TFrom, TTo>:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe implicit operator Vector3(ValueTuple<float, float, float> value)
{
    return Unsafe.As<ValueTuple<float, float, float>, Vector3>(ref value);
}

It doesn't really matter, but the Jit emits an extra mov instruction for your pointer version for whatever reason.

MeikTranel commented 5 years ago

This will make plain text code search for constructor calls impossible without complex RegEx queries... Just saying.

jnm2 commented 5 years ago

@MeikTranel I don't find myself taking advantage of plain text searches for constructors today. It's too easy to Ctrl+T to the constructor of the type I want and then press Shift+F12 to find all references.

MeikTranel commented 5 years ago

Ideally this should be the case yes, but for very large multi-solution workspaces this simply is not possible. I personally just don't see any benefit to this proposal that would be worth any kind of downside. I mean essentially we're talking about moving a type from the right to the left side... and making future constructors with a bunch of constructor bodies completely unreadable.

EDIT: It feels to me like the solution here solves the problem to generally - "newing up fields in a class should be made less verbose" seems to be the requirement. Changing all that is holy about C# newing is the proposed solution.

CyrusNajmabadi commented 5 years ago

This will make plain text code search for constructor calls impossible without complex RegEx queries... Just saying.

var makes plain text search for a type-name impossible. C'est la vie. Brevity is naturally at odds with explicitness. If that's a problem for your style of development, don't use the features that allow you to be briefer :)

CyrusNajmabadi commented 5 years ago

and making future constructors with a bunch of constructor bodies completely unreadable.

Why is it unreadable?

Changing all that is holy about C# newing is the proposed solution.

Given the LDM doesn't think this is the case, perhaps you are imposing your own view on what is "holy" here...