csharplang
csharplang copied to clipboard
Champion: using aliases for any types (VS 17.6, .NET 8)
- [x] Proposed
- [x] Prototype: https://github.com/dotnet/roslyn/pull/50167
- [x] Implementation: Done
- [ ] Specification: Proposal: https://github.com/dotnet/csharplang/pull/5174
Summary
Simply put,
using T = System.Collections.Generic.List<(string, int)>;
is legal. While
using T = (string, int);
is not. I suspect this was simply an oversight, but either way there seems no good reason that it shouldn't work (and it parses terribly and does not produce a good error message either).
Motivation
Language consistency, completing things we started. Without this syntax you cannot give tuple names either, which is a big downside.
Language Change
The grammar will be changed like so:
using_alias_directive
- : 'using' identifier '=' namespace_or_type_name ';'
+ : 'using' 'unsafe'? identifier '=' namespace_or_type';'
;
Concluded Questions
-
Similarly, what are the semantics of a pointer type in an alias. e.g.
using XPtr = X*;. Using aliases are in a location that generally cannot be markedunsafe. So we'd likely want this to be legal, with an error if you then use XPtr in an safe context.Answer: Aliases to pointers are allowed. However, they must be written in the form
using unsafe X = T*;- Using a pointer type in an alias not marked with
unsafewill be an error. - It will be an error if
using unsafe X = T*;is written andTis not a type that is valid to take a pointer to. Regardless if 'X' is not referenced anywhere in the code. - It will be an error if
using unsafe X = T*;would be ok, but the user has not passed the/unsafeflag to the compiler.
- Using a pointer type in an alias not marked with
-
What are the semantics of a NRT nullable type with an alias. e.g.
using X = string?;. Specifically, does the alias have to be an a#nullable enableregion? Or can it be located anywhere. Then, what does it mean if that alias is used in a context that doesn't allow nullable? Is that an error, a warning, or is the nullable annotation silently ignored?Answer:
- It is an error to have an alias to a top-level NRT type. e.g.
using X = string?;is not legal. using X = List<string?>;remains legal as there is no top-level NRT, just an interior NRT.using X = int?;remains legal as this is a top-level value type, not a reference type.
- It is an error to have an alias to a top-level NRT type. e.g.
-
For unsafe code, should the syntax be
using unsafe X = int*;orunsafe using X = int*;. i.e. does theunsafemodifier come before or after theusingkeyword.Answer (LDM meeting 2/1/23):
- The syntax will be
using unsafe .... This keepsusingblocks consistent withusingalways being the first token.
- The syntax will be
LDM Discussions
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-09-20.md#type-alias-improvements https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#using-aliases-for-any-type https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#ungrouped https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-11.md#using-aliases-for-any-types
I'll dual champion this :-)
Tbh, I'm fine with any proposal of the form: using id = type_syntax;
I'm also willing to do the impl work here. I can think of some nice ways to do it that would be easy to add.
Closing a duplicate of https://github.com/dotnet/csharplang/issues/1239. See https://github.com/dotnet/csharplang/blob/a3d67d141501101af9dbb9e8192ca8b4c717e316/meetings/2020/LDM-2020-07-13.md#generics-and-generic-type-parameters-in-aliases, where we explicitly mentioned adding support for tuple syntax, and https://github.com/dotnet/csharplang/blob/87adc18115ce4b4429610af1330ca4430219ddd6/meetings/2020/LDM-2020-09-28.md#proposal-support-generics-and-generic-type-parameters-in-aliases, where the timing was discussed.
Not quite following how the other issue subsumes this one -- is it purely because the proposed syntactic alterations include namespace-or-type-name on the RHS?
I would prefer we break out this narrower proposal. The linked proposal is very broad and contains extra things i don't think are necessary to solve the core problem.
The linked proposal is very broad and contains extra things i don't think are necessary to solve the core problem.
Personally, my read of the opinion the last time we talked about this was that we're not sure how far we want to go. The proposal itself was relatively simple: permit more things in using aliases. This includes generic types, keywords, and tuples (@agocke this is the part that subsumes this proposal). I wouldn't want to consider a narrower proposal without considering the broader one at the same time.
I'm going to reactivate this. POtentially bringing back to LDM to discuss. This is a narrower slice than the braoder topic we originally talked about. Importantly, this is extremely cheap to implement and solves a broad swath of the cases, without having to go into much more complex areas like using X<T> = List<T>;.
Prelim LDM notes:
- we should treat the aliases in a semantic (not syntactic) fashion. That means we should not think of the alias as a literal copy/paste of the RHS to the use site. So if you had
using X = string?then it should be fine to useXin a#nullable disableregion. It would behave the same way that it's legal to haveusing X = List<string?>and then use that in#nullable disable.
@CyrusNajmabadi does this include aliases for "open" generic types, e.g.
// Straightforward:
using Foo<T> = List<T>;
// Value-tuple:
using Bar<T> = ( String a, Int32 b, T c );
or
// Straightforward:
using MultiDict<TItem> = IReadOnlyDictionary<Int32,IReadOnlyList<TItem>>;
// Value-tuple:
using MultiDict2<TItem> = IReadOnlyDictionary<Int32,IReadOnlyList<(String a, TItem b)>>;
@Jehoel It does not.
@Jehoel There is a separate proposal for exactly that: #1239
Based on the proposal, would anonymous types also be allowed?
(I don't see where (value) tuples are in the standard to define type.)
E.g. allow replacing:
var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
And then referencing sampleANonLR.GetType() with
using TAnonLR = new { TLeft left, IEnumerable<TRight> rightg };
And then referencing typeof(TAnonLR)
I guess there is no way to write an anonymous type as a type as oppose to a value...
No, the proposal as written would not allow for this. Anonymous types have no user expressible name/syntax taht you can use to refer to their type.
@CyrusNajmabadi Anonymous types have no user expressible name/syntax that you can use to refer to their type.
...which is kinda silly now in 2022: It's a shame that Anonymous Types were originally C# 2.0 to simplify code and allow C# users to be more expressive without the ceremony of fleshed-out class type definitions, but because the design of ATs (and their atrocious ergonomics) seemingly haven't been touched since 2005 it means that any time-savings of using ATs are eliminated by the fact you need to revert back to using ceremonious class definitions if you want to do anything useful with ATs:
- Implement an
interface. - Participate in inheritance or composition.
- Target them for compile-time modification (Fody, etc).
- Return them from a function.
- Allow mutable properties.
I do understand why there were these restrictions back in C# 2.0: insufficient dev-time budget for release-day improvements, "less is more", and wanting to see how C# programmers would end-up using anonymous-types to see if further investment in improvements is worthwhile.
While it's now clear that C# 4.0's:Tuple<>, and C# 7.0's ValueTuple<> and C# 9.0's record class types are all simply better at being low-ceremony types compared to ATs, unfortunately there's plenty of showstoppers:
I made a quick comparison table of the main different kinds of product-type/record/tuple in C# today, which I think demonstrates the gap that ATs fall into:
| C# Version | Definition tedium | Has "real" named properties | EF/Core Linq projection | Exportable | Implement interface |
Custom ctors and class invariants | |
|---|---|---|---|---|---|---|---|
Mutable class |
1.0 | Medium | Yes | Yes | Yes | Yes | Yes, but not with EF: must use mutable properties in projections. |
Immutable class |
1.0 | High | Yes | Not supported | Yes | Yes | Yes, but not supported by EF at all. |
| Anonymous Type | 2.0 | Low | Yes | Yes | No | No | No |
System.Tuple<> |
4.0 | Low | No | Not supported | Yes | No | No |
System.ValueTuple<> |
7.0 | Low | No. Member names set with attributes. Unsafe. | Not supported | Yes | No | No |
record class |
8.0 | Low | Yes, but no camelCasing of param names | Not supported | Yes | Yes | Yes, but this is surprisingly difficult, and often requires long-form ctor definitions, which eliminates one of record types' main ergonomic advantages: terseness. |
- Footnotes:
- EF Core requires ATs or POCOs for projections to non-entity types: it explicitly does not support
record classtypes for reasons that make no sense to me (why can't they useObject.ReferenceEqualsand just disregardoverride Object.Equals?) - I make it a point to remind people that constructors in OOP are exactly how you implement class invariants.
- (For background): Invariants (preconditions, postconditions, etc) allow you to make hard guarantees about object-state. That is, if
class Foohas a ctor that enforces invariants (e.g. by throwing during construction if there's a parameter validation error) then functions with aFoo-typed parameter can safely depend on those invariants - and this doesn't require full immutability either - overall it makes for much easier-to-understand software with fewer bugs caused by human misunderstandings. - ...so it's very frustrating to me that the C# language doesn't make it easy to define or use custom constructors: so much of C# and its ecosystem doesn't seem to have a problem with default-constructors that leave
newobject instances in an invalid-state which must be manually late-initialized somehow (I'm looking at you WinForms and WPF, thenew()generic constraint, Entity Framework, and others), evenrecordtypes in C# 9.0 are surprisingly difficult to add constructor parameter validation to, despite obvious massive utility.
- (For background): Invariants (preconditions, postconditions, etc) allow you to make hard guarantees about object-state. That is, if
- EF Core requires ATs or POCOs for projections to non-entity types: it explicitly does not support
So looking at the table above, every product-type-kind in C# has its own deal-killers: some of which are unavoidable (e.g. the lack of safety with ValueTuple if you get careless with member names), but some current deal-killers, as listed above, certainly (in my opinion) can and should be rectified somehow - so without getting too side-tracked from this thread's original topic of using aliases, I'd like to suggest:
- Biggest positive impact: Force the EF Core team to support
recordtypes in query projections (and ideallyValueTupletypes too).- This will eliminate probably the single main use-case for ATs today, paving the way for the feature to be deprecated or even removed in future, as ATs should be entirely redundant given we now have
recordtypes andValueTuple.
- This will eliminate probably the single main use-case for ATs today, paving the way for the feature to be deprecated or even removed in future, as ATs should be entirely redundant given we now have
- If the EF Core team remains intransigent then for the next biggest positive impact: make ATs optionally named types and participate in
partial classdefinitions, which in-turn means we can finally make ATs exportable (i.e.publicorprotected) and have ATs implement interfaces and participate in inheritance and composition.- ...and making ATs optionally named is what @NetMage's reply is suggesting - and while I support the idea, I'm not yet sold on the
using x = new { ... }syntax. - Of course, doing this will just mean that ATs will become just a worse
record-type, with zero advantages besides exclusive EF support - but would at least represent a forward step towards hopefully eventually removing ATs?
- ...and making ATs optionally named is what @NetMage's reply is suggesting - and while I support the idea, I'm not yet sold on the
@Jehoel this appears to be out of scope for this proposal. This proposal is about allowing a using alias to any existing type you can already express in C#. You cannot express anonymous-types in c#, so that's not something covered here. If you'd like that to be possible, please open another issue to track that. If it happens, it would fall out that it would then be supported in an alias.
that said... teh point of anonymous types is to be... anonymous. If you're going to give them a name, just write a simple record and you'll be good :)
So looking at the table above, every product-type-kind in C# has its own deal-killers:
Sure. that's why we have options. There is no goal in the language, or surrounding ecosystem for one perfect type that solves all needs for all users in all cases. Pick what makes sense per domain :)
Biggest positive impact: Force the EF Core team to support record types in query projections (and ideally ValueTuple types too).
This is the repo for the C# language. We are not teh EF core team, nor do we force anyone anywhere to do anything. If you would like another team to make a particular change, you can ask them and they can make an appropriate decision for their domain if that's appropriate. It's not our place, or desire, or ability, to force anything.
then for the next biggest positive impact: make ATs ...
Please open tracking discussions on any ideas you have around anonymous types. This issue is not the right place for that discussion. If there is enough interest from the community or an LDM member, it may happen. However, writing off topic posts on unrelated issues is not appropriate and will not change anything here for that other feature.
@Jehoel this appears to be out of scope for this proposal.
Yes, I did get carried-away again...
This proposal is about allowing a
usingalias to any existing type you can already express in C#. You cannot express anonymous-types in c#, so that's not something covered here.
If you'd like that to be possible, please open another issue to track that. If it happens, it would fall out that it would then be supported in an alias.
Fair enough
that said... the point of anonymous types is to be... anonymous. If you're going to give them a name, just write a simple record and you'll be good :)
But my point is that we can't use record types as a replacement for ATs currently.
That's why we have options. There is no goal in the language, or surrounding ecosystem for one perfect type that solves all needs for all users in all cases. Pick what makes sense per domain :)
The problem is that many of these deal-killers are entirely artificial constraints that really shouldn't be there anymore after 17 years. Yes, we have different options for different scenarios and there is no single magic solution for all use-cases that would make everyone happy, but at the same time, you can make a lot of people less unhappy with fewer artificial restrictions.
This is the repo for the C# language. We are not th EF core team, nor do we force anyone anywhere to do anything. If you would like another team to make a particular change, you can ask them and they can make an appropriate decision for their domain if that's appropriate. It's not our place, or desire, or ability, to force anything.
My request to "force" the EF team was facetious - but let's not pretend that some teams have plenty of influence over other teams - I'm basically asking someone on the inside to send a persuasive email to EF so we get a fix out in a couple of weeks instead of the months-to-years for a community-requested change to EF.
Please open tracking discussions on any ideas you have around anonymous types.
Yessir!
This issue is not the right place for that discussion.
Correct!
If there is enough interest from the community or an LDM member, it may happen. However, writing off topic posts on unrelated issues is not appropriate and will not change anything here for that other feature.
I don't think it's fair to describe my post as off-topic: I was expanding on @NetMage's on-topic suggestion and contributing alternatives.
(Ideally GitHub issues would be like GitHub Discussions with tiered/threaded conversations where I could reply to other people without seeming off-topic).
But my point is that we can't use record types as a replacement for ATs currently.
Please open discussions on anything related to that. that is unrelated to the goal of allowing a using alias to have any current C# type on the RHS of hte alias.
but at the same time, you can make a lot of people less unhappy with fewer artificial restrictions.
Again, the process for getting changes into the language is to open discussions on things like you'd like to see changed. We prioritize based on value and feedback. Complaining on random other proposals does not change this.
I don't think it's fair to describe my post as off-topic: I was expanding on @NetMage's on-topic suggestion and contributing alternatives.
Netmage asked a question if this was on topic or not. I stated it wasn't. It's fine to ask. It's not fine to continue on this :)
(Ideally GitHub issues would be like GitHub Discussions with tiered/threaded conversations where I could reply to other people without seeming off-topic).
No, our issues are for tracking the actual course of a proposal to implementation (please see our repo guidelines), they are not for free-form conversation about different topics. Please follow our rules while you're here, i imagine you'd like peopel to do the same in your repos :)
Does this feature intend that the type alias is persistent (still valid when accessed from another namespace)? What I mean is ... If I have :
namespace A {
using Unit = System.ValueTuple;
}
and in another file I have:
namespace B {
using namespace A;
// currently, Unit is not a thing in this scope... I need to repeat the same using statement _using Unit = System.ValueTuple_
}
Also, currently the documentation states that when you alias with using, the type names are interchangeable (Unit / System.ValueTuple). Now If I write specialized behaviour for Unit (ex: extension method), it is also applicable to System.ValueTuple, and this is something I don't want.
Currently the only way to get this outcome (for both cases above) is to derive:
public struct Unit : System.ValueTuple {}
Is there a different proposal if this one does not address these ? Thanks!
@CosminSontu
Does this feature intend that the type alias is persistent (still valid when accessed from another namespace)?
No, this proposal does not change the scope in which aliases are defined.
If you want an alias to be defined across an entire project you can use global aliases.
ITNOA
@CyrusNajmabadi Is this proposal, allow use using alias in another using alias in same scope? (as you can see in #3873) for example
using MyType = List<int>;
using MyDictionary = Dictionary<int, MyType>;
thanks
No. It is not.
@CyrusNajmabadi So I think it is good to adding allowing this in this proposal, because when I can using aliases for anything, so I can using aliases for another using aliases.
And it is meaningless when using alias aliasing the type, I cannot using the alias type in every where that I can use type.
@soroshsabz this feature is already complete. We are not intending on changing the design here. If you'd like a new set of features, please open a discussion on that topic.
To be clear, this feature was simply about expanding the set of types allowed on the RHS of an alias to all types. That's it. :) Anything else is outside of the scope of it.
I cannot using the alias type in every where that I can use type.
You def can. For example:
using X = Whatever;
namespace N
{
using Y = X[]; // can reference 'X' here.
}
It's simply that within a particular set of usings, the aliases defined in that set are not available within that set itself. But that's no issue as you can just provide the exanded form. For example:
using X = Whatever;
using Y = Whatever[];
You may have some duplication in the using-block, but you can still use those aliases interchangeably at the use sites. Given that you only need to declare once, but might reference any number of times, this is totally reasonable.
@CyrusNajmabadi thanks for details response, the problem is, we cannot use global using into namespace in correctly, so when I have global using Foo = List<int>; I cannot using global using Bar = Dictionary<string, Foo>.
Right. But that's fine. Just write:
global using Bar = Dictionary<string, List<int>>. It's the exact same thing.
@soroshsabz, I think you have a good point, but you should probably start a new discussion or new issue about it, since this issue is feature-frozen 🙂
@Eli-Black-Work I add new discussion in https://github.com/dotnet/csharplang/discussions/7253
thanks