Open AlekseyTs opened 4 years ago
I don't see the value-add of this - it lessens the ability of readers to reason about the C# source code their reading by adding mystery.
Far from being mysterious, these chunks at the tops of each file are often large and obvious, and they are rarely interesting to look at. They are a frequent source of noise in diffs where they are usually just as uninteresting as when reading a source file.
There is no option for you if any of this bothers you today. The IDE could have an autocollapse feature for using directives, but that wouldn't do anything to help other experiences. The number of times this has come to mind has overcome the skepticism I had on this a few years ago. I'm happy to see that an option is being provided.
@louthy might find this interesting for his language-ext package. Currently you have to add
using LanguageExt;
using static LanguageExt.Prelude;
To every file. If you could specify in the csproj that every file should have those preimported it would make his language extensions feel much more idiomatic.
I often came to the point wanting a project wide type alias: using xxx = sometype;
This desire was mainly driven by library development purposes, not by usage aspects.
A project wide using static could be smart also. (but I had never a real need for this)
I often came to the point wanting a project wide type alias:
I feel less comfortable about this. I feel like if C# ever introduces project wide type aliases, it should be a first class language feature, rather than a compiler switch.
My opinion: I really like _Imports.razor
from ASP.NET Core Razor framework; I'd like to see something similar for C# as well. :)
Instead of cli option, could be considered as global aliases. There is a handful of proposals in that space already. For instance:
// Imports.cs
global using System;
global using static Helpers;
global using Alias = TypeOrNamespace;
On top of that, https://github.com/dotnet/csharplang/issues/1239 is championed as well which could be used together.
global using ServiceResult<T> = Result<T, ServiceError>;
An important question would be metadata encoding outlined by @333fred in https://github.com/dotnet/csharplang/issues/259#issuecomment-568916708
I also support the concept of global usings in the project file. Although there'd be no way top opt out of these auto-usings per file, so it's possible there'd be clashes. There are ways around this though, so maybe it's not too bad.
As noted above, VB.NET already has this (and has since inception), and it has never been considered a source of confusion. Most users would interact with it via their project settings. I personally don't find it to be any different or more confusing than the fact that you can add references from the command line which has just as profound impact on the code you are reading.
@alrz
On top of that, #1239 is championed as well which could be used together.
If that proposal is also implemented it might complicate this proposal as it involves characters that can't be used on the command line. Replacement characters would be required. It wouldn't be a big deal, but something to keep in mind.
🍝
-using:StringDict(T)=System.Collections.Generics.Dictionary(System.String,T)
I expect generic type aliases to be able to define generic constraints. Also it could be a possiblity to enable "public" type aliases in the future. This proposal doesn't make the transition any easier.
I don't think there's any sane way to encode that in an string except for literally writing a possibly slightly diffrent C# dialect inside the command line, plus you'd lose any IDE goodies like autocomplete or goto declaration. That will need an expensive tooling support for a trasparent experience.
We're practically exposing language semantics outside of the language itself to an external source - this doesn't seem like a good idea IMO.
@alrz
I expect generic type aliases to be able to define generic constrains. Also it could be a possiblity to enable "public" type aliases in the future. This proposal doesn't make the transition any easier.
That's not a part of that championed proposal, although it's fair to bring it up as a possibility. I'd think that this feature would be orthogonal to public aliases. It's also much simpler to implement, and it doesn't necessarily have to support the full breadth that using
declarations might support.
That will need an expensive tooling support for a trasparent experience.
VB.NET already (and has always) exposed this option through the tooling experience in Project Settings. IMO that's not a problem.
I'd think that this feature would be orthogonal to public aliases. It's also much simpler to implement, and it doesn't necessarily have to support the full breadth that using declarations might support.
I believe it's actually very similar to an "internal using directives" feature which itself is a subset of global imports. All the mentioned features could fit nicely in an "enhanced using" umbrella, but this proposal as a command line option does not contribute to that possible set of improvements and likely becomes unpreferable when we have those features in place.
VB.NET already (and has always) exposed this option through the tooling experience
For me, it's understandable if VB does that. I think VB is a lot more "forgiving" in different aspects than C# (more implicit conversions, non-constants in case clauses, to name a few). So it's accepted that we even have a few imports by default. Maybe it worked out well for the target audience, but C# has always been a little more strict.
My argument is that any code that has meaning e.g. affects binding, should be a part of the source. The second you're out of source, the experience tends to degrade, in some sense.
Whether this is implemented via new C# syntax or CLI parameters, the SDK will be able to build on top of this and do <GlobalUsing Include="System" />
csproj items.
Hopefully the LDM discussion on this subject will include global aliases and maybe exported aliases.
IMO the experience isn't any different if the namespaces/aliases are declared in the CLI vs. in a source file that could potentially be very far removed from the source file that you're currently looking at. I stopped using VB.NET around the time that C# 3.0 was released but I do know that I made use of project-wide imports.
I don't agree with this statement:
As noted above, VB.NET already has this (and has since inception), and it has never been considered a source of confusion.
Even on roslyn itself it has been a point of pain and confusion numerous times :)
@CyrusNajmabadi
Even on roslyn itself it has been a point of pain and confusion numerous times :)
Sounds like a good point to bring up in LDM. But is the nature of the feature confusing, or is it confusing because it's an aspect of VB.NET and not C#? As a VB.NET user for some time I never found it confusing. I found it more confusing that C# didn't support it. I'd be interested to hear a poll of VB.NET users.
Anything that would be global would defeat a large number of use cases.
At the very least any sort of feature like this would need to be opt-in, either on a per-code-file or per-folder basis like the razor features that are mentioned.
At least this holds for how I use #defines
if I use many of them and I see a lot of requests where this is used to change basic types, but while a compiler switch or other global configuration may work for applications I reckon it will create major headaches trying to extend this into libraries.
// MyCommon.cs
namespace MyCommon {
public using System;
public using MyTypeA = Quux.MyTybe<A>;
public using ...;
}
// Foo/Bar.cs
using MyCommon;
// MyTybeA ...
// ...
The above:
public using
s under an the top namespace)using
bundles"using
declarationsPersonally I don't like the idea of "invisible" using
s. They're invisible because usually one doesn't need to play with the build system often. And the core idea of reducing the number of lines of using
s has so much more potential.
@narfanar
I'm not sure that approach is any better. Those "using"s are just as invisible, now they're just packaged in other namespaces. Now importing a single namespace could also introduce any number of other namespaces or aliases which could introduce ambiguities or cause code to be silently reinterpreted. At least a project-level setting is explicitly determined by the developer.
Something i forgot to mention in teh LDM meeting:
IDE has already put in a bunch of features to make 'usings' less relevant. For example, a common reason specified for having these usings
automatically added is so that you can immediately just start using the types/extensions from it without needing to manually add the using
first. However, IDE has a feature (which we've just switched to being 'on by default') whereby we will include all members from all namespaces in teh completion list, and will add the using
for you automatically if you commit it.
This makes it easy to browse and find things (i.e. you can just type Xml
or File
or Stream
) and then add what you need for that file.
As such, an empty file is a totally reasonable place to start using C#. With top level statements you can have that, and just type what you want (adding imports as necessary for the things you use).
I strongly dislike the idea that a C# source file will suddenly behave differently based on what compiler options are specified (save checked
/unchecked
). It's already the case that examples of code on the web often leave out using
statements - making the code ambiguous to the readers if types in the code exist in more than once namespace. Further encouraging people to leave out using
s because they are now hidden behind compiler flags seems like a step backwards.
In any modern IDE it entirely takes care of the usings section for you- adding usings as needed, offering to remove unnecessary ones.
@MgSam - good points on the issues with making a build dependent on non-code based command line parameters.
That said, I would hope we do not lose sight of the fact that the "ideas" presented here (and the problems they are trying to solve) are still valid, e.g., trying to find some to option to provide typedef
style functionality with a "global" using alias would be a great add (imo).
As mentioned above, language augments are always an option. I sort of like the option mentioned to apply global using statements to the XML project file - seems like a simple, safe solution, and begins to fit your idea of a modern IDE "taking care of it for you".
Maybe the powers that be can consider the options discussed here.
I think I had some similar discussion a while ago: https://stackoverflow.com/questions/61813506/implicit-assembly-wide-using-directive/61813769#61813769
Take a lesson from VB and please add this. There is only confusion if done wrong.
This seems like a real step backwards and archaic. Please don't add this. What problems does this solve?
Code readability (at the risk of introducing ambiguity if done badly). Adding System.Threading.Tasks, System.Linq, System.Collections.Generic to every file to use async/await or .FirstOrDefault or List
@LeonG-ZA at least with a global using alias, e.g., using StringMap = System.Collections.Generic.Dictionary<string, string>;
you basically get a free typedef
It makes the cognitive complexity a lot worse. Types matter. When scanning through code you can immediately know what Dictionary<string, string> is, but StringMap someone unfamiliar with the code will have to pause and hover over it to see what it actually is. The more typedefs there are the more you have to think what is happening under the covers. I've worked with C++ over 10 years ago and it was one of my Pet Peeves was that typedefs were misused like this. It makes sense in certain situations, but this definitely isn't one of them.
Namespace wide type aliases are especially useful when using generic type arguments
I have hundreds occasions of IFoo{int}
What if I choose to use Guid or long instead? I have to a) change every occasion (search and replace = bad) or ... b) define an alias for IFoo{int} and use that alias in every file (not feasible for 100+ files) or ... c) define an typed interface ITypedFoo : IFoo{int} and make sure every class inherits this new interface as well so I can use it properly I went for c) but it's not an optimal solution. A namespace-wide alias would solve the whole situation properly without any effort on developer side.
It makes the cognitive complexity a lot worse. Types matter. When scanning through code you can immediately know what Dictionary<string, string> is, but StringMap someone unfamiliar with the code will have to pause and hover over it to see what it actually is. The more typedefs there are the more you have to think what is happening under the covers. I've worked with C++ over 10 years ago and it was one of my Pet Peeves was that typedefs were misused like this. It makes sense in certain situations, but this definitely isn't one of them.
Isn't that encapsulation though? Hiding implementation details when appropriate. It's the responsibility of the programmer not to misuse features like typedef. Granted, I don't know why would you have StringMap, but I could have domain-specific UserToEmailMap defined that way, and the reader shouldn't need to know it's based on Dictionary. Instead, I end up writing wrapper classes.
A namespace-wide alias would solve the whole situation properly without any effort on developer side.
This is a major and compelling reason why namespace-wide aliases would be a bad idea.
If you're making a fundamental change like the one you describe, every use needs to be inspected to see if there are unexpected consequences - integer overflow, formatting, PII disclosure, etc.
Using type aliases to solve this problem is about as safe as doing a global search and replace with a text editor, with all the dangers that implies.
Using replace with a text editor ist much more dangerous as it does not deal with name ambiguities in any way and therefore should be avoided if possible.
I use search and replace regularly. It pairs well with reviewing the diff immediately afterwards.
This isn't a feature to be used everywhere and by everybody. It's a feature for specific situations.
Just like unsafe
and many other C# features it has benefits for a subset of projects and it is possible to misuse it. I rarely use unsafe
, but it's essential when I need it. Global usings are much the same. Most projects should avoid them, but they'll be greatly beneficial for some projects and therefore have a place in a future version of the language.
@danielcrabtree
I'd argue that you're giving a great reason to not include them in the language. Unsafe, while somewhat niche, is very narrowly focused and requires syntax scoped to where you are using it. Project-wide usings/aliases, by definition, have a blast radius across an entire project. If most projects shouldn't use a project-wide feature then it probably shouldn't be a project-wide feature.
I would use them in almost every project if I could. I've started adding types to primitives such as identifiers. A lot of strings and integers could be strongly typed structs. So instead of int id
I could have UserID id
. But C# is making this pattern very difficult, especially when dealing with structs since they don't support inheritance. Even with classes it would be useful to have the concrete ID type isomorphic with the base type (UserID == ID<User>
), to make better use of generic factories for example.
I bet a lot of people would use type aliases. Even Hanselman is suggesting that in his blog! But I can't understand how can someone have the same using alias in every file.
@jukkahyv
So instead of
int id
I could haveUserID id
.
The problem I see with using aliases in this manner is that they are completely erased and offer zero type safety. The compiler will offer you no help if you accidentally pass a UserID
to a method that accepts a CustomerID
or an ItemID
, etc. The alias only offers a false sense of security but is functionally just as brittle as using int
everywhere. For these use cases instead of enabling and encouraging type aliases as they exist today I'd rather see language features that offer the ability to declare type-safe aliases or zero-cost wrappers. Struct records might offer that out of the box, e.g. public record struct UserID(int value);
. Roles might also.
but it's essential when I need it. Global usings are much the same.
I don't see how it can be essential given we've have 20+ years of not having it, and people have been able to be totally successful without them all this time. I think we're well past the point that anything is actually essential in the language.
Lets not take my analogy with unsafe
too far. I was merely using that as an example of a language feature that's highly useful in certain niche scenarios and that despite most projects not using or needing it, it's a useful addition for some use cases.
@HaloFour I agree that this feature should be scoped, rather than implemented as originally suggested. My preference would be something along the lines of the syntax suggested by @munael. However, I would want the option to use it without adding a reference such as using MyCommon;
, but with stricter scoping rules to limit the blast radius as you call it. For example:
namespace MyApp.FeatureA {
internal using System;
internal using MyTypeA = Quux.MyTybe<A>;
internal using ...;
}
This would apply those using statements to any code within the internal scope, i.e. the current project/assembly, provided that code is within the MyApp.FeatureA namespace (including descendant namespaces, e.g. MyApp.FeatureA.SubFeatureB).
Let me just chip in with my opinion.
using
statements, which then apply to code files as well - so as if these files were #include
ed (without all the bad side effects of actually including it textually) after any potentional #define
statements in the individual files. I will shortly also post some code that shows how that becomes rreally handy.I think the current using would be more readable and useful if
using List = System.Collections.Generic.List<float>
than to have to use System.Single
instead of float
.using X = Y
and using static
syntax. They do today when declaring e.g. using T = System.Double
you can use T
later on, but they don't accept Double
even if using System
appeared previously. The example of declaring some ImmutableDictionary<K, V>
becomes much cleaner and more readable using this ability, especially if K
and V
are types declared deep inside some namespace tree.(short x, short y)
. Probably conditional on compiler support for tuples in the first place. Today you can use ValueTuple<short, short>
but now you can only have generic names for the tuple elements.ushort
) before than hacking back some random using back into tooltips etc but it might be different as the alias that was declared say for a specific function argument (e.g. WordDWordOrQWord
instead of Offset16
for ushort
).I hate copying ever more complex blocks of using
code between an increasing number of files.
Let me add some code which I hope whatever is cooked up here will natively and fully support and simplify as far as possible.
The example is very short, you could say useless, but it's just a short example. I have a few projects that have files with over 100 lines following this pattern and special build tasks that copy files just adding a single #define
to create the variants that are missing from the source code tree - that is, all except one.
That last feature is the biggest of it all imho because it allows the best experience editing code for a specific scenario and to switch between scenarios by simply changing one #define
or a single MSBuild property in the project file.
// MSBuild adds this line in copies of the file, excluding the one that is special cased
#define HALF
// Special case one to have the best experience editing this variant in Visual Studio - I have to also edit the project file to synchronize changes to this section
#if !(HALF || SINGLE || DOUBLE)
#define HALF
#define SOMETHING_ELSE // Don't be fooled by the simplicity of this example
#endif
// MSBuild copies the rest of the file - would be nice to have another feature to make this automatic, too
#if HALF
using Component = System.Half;
using Float = System.Single;
using static System.MathH; // I wrote this myself, don't go look for it anywhere -- also not used, the example is too short
#elif DOUBLE
using Component = System.Double;
using Float = System.Double;
using static System.Math;
#else
#error Missing appropriate #define
#endif
namespace Colors {
public partial struct Rgb {
public Component R;
public Component G;
public Component B;
public Component Intensity => (Component)((0.21 * (Float)R) + (0.72 * (Float)G) + (0.07 * (Float)B));
}
}
Color.cs
As simple as this example is, I have lots of cases where structs and their fields are declared in one file, overrides and operators in another file, binary read and write functions in yet another file, etc. In a more complex code resembling this, I have 160 lines of usings intermingled with #if
etc, followed by #if
's around things like class name declarations and usually a few or preferably no #if
anywhere in the actual code.
It makes the cognitive complexity a lot worse. Types matter.
I sympathize but completely disagree. This feature may not be suitable always but it should reduce cognitive complexity by allowing brief and concise declarations, once. Don't repeat yourself. For example, especially with the Immutable
family of types, I find I usually have just one in the whole file and the total cognitive overload of some types I sometimes have to declare and re-declare and then copy/paste to new them up is total bloat. Target typed new solves some of that but by far not all of it.
Code becomes a lot easier to read with a single, short, descriptive name without any generic syntax in this example. Plus especially when changing the type slightly, the cognitive overload is that the full, generic names are just a bitch to compare or to edit.
It makes the cognitive complexity a lot worse. Types matter.
I sympathize but completely disagree. This feature may not be suitable always but it should reduce cognitive complexity by allowing brief and concise declarations, once. Don't repeat yourself. For example, especially with the
Immutable
family of types, I find I usually have just one in the whole file and the total cognitive overload of some types I sometimes have to declare and re-declare and then copy/paste to new them up is total bloat. Target typed new solves some of that but by far not all of it.Code becomes a lot easier to read with a single, short, descriptive name without any generic syntax in this example. Plus especially when changing the type slightly, the cognitive overload is that the full, generic names are just a bitch to compare or to edit.
I disagree. I read the using statements to get a general idea what is being used. I guess a virtual/generated section by the IDE at the top of the file can be a compromise. Viewing the file without an IDE will be an issue.
Referring to resource files/classes in different assemblies makes it necessary to have a descriptive alias for each Resource class, as they're usually named 'Resources'.
It's nasty to define all used aliases in every file where it could be the case:
using ResCommon = Common.Shared.Properties.Resources; using ResCommonUI = Common.Server.UI.Properties.Resources; using ResCompanyX = CompanyX.Resources.Properties.Resources; using ResBrandY = CompanyX.BrandY.Resources.Properties.Resources;
Assembly-wide usings / type aliases are already usable - in blazor (.razor files). There is a reason why blazor allows that "concept" in _Imports.razor:
@using ResCommon = Common.Shared.Properties.Resources @using ResCommonUI = Common.Server.UI.Properties.Resources @using ResCompanyX = CompanyX.Resources.Properties.Resources @using ResBrandY = CompanyX.ResBrandY .Resources.Properties.Resources
If a separate file or other way of implementing this does not matter for me. I also like how VB.net allows to define application wide usings
Some state, that assembly-wide usings are dangerous. Well, so they are in vb.net then for almost 20 years now. Is it a problem? I don't see why having in other .net languages it's not a problem, but in C# it is.
@333fred in your meeting you mention the solution used in blazor as the exception but I can't see that you address the _import.razor pro and cons.
@333fred in your meeting you mention the solution used in blazor as the exception but I can't see that you address the _import.razor pro and cons.
Hmm? We didn't talk about blazor in any fashion.
@333fred
Hmm? We didn't talk about blazor in any fashion.
I misread the exception part. (Larger projects != Blazor).
I asked since your meeting notes were linking to this issue where the last comments mention the _Import.razor approach. It would have been interesting to read about your ideas regarding forcing global using to be in one single file, either by convention or by a command line flag pointing to the file rather than the using namespaces themselves.
@333fred
Hmm? We didn't talk about blazor in any fashion.
I misread the exception part. (Larger projects != Blazor).
I asked since your meeting notes were linking to this issue where the last comments mention the _Import.razor approach. It would have been interesting to read about your ideas regarding forcing global using to be in one single file, either by convention or by a command line flag pointing to the file rather than the using namespaces themselves.
We didn't discuss that during the meeting.
The compiler will offer you no help if you accidentally pass a UserID to a method that accepts a CustomerID
I think this argument contradicts to what "aliases" mean in the language today.
If UserID is actually a separate type with it's own behavior you need a struct, not an alias.
Even if public aliases were possible, it's still just that, it shouldn't be preserved in the emitted code IMO.
Many (most!) of those arguing for global aliases want them because they want to define e.g. CustomerId
as an alias for Guid
- but the lack of type safety means that this is essentially useless. They'd have the illlusion of a new type but with none of the actual benefits.
If UserID is actually a separate type with it's own behavior you need a struct, not an alias.
I agree wholeheartedly. With the introduction of record in C# 9 (and hopefully record struct in C# 10) writing these little types is becoming very little work for a very large gain.
Motivation - provide shared context for the program to reduce repetition of common using directives across all source files. For example,
using System;
, etc. VB compiler supports that for many years.Specification: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/GlobalUsingDirective.md