dotnet / csharplang

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

[Proposal] Add ability to declare global usings for namespaces, types and aliases by using a command line switch #3428

Open AlekseyTs opened 4 years ago

AlekseyTs commented 4 years ago

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

theunrepentantgeek commented 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.

jnm2 commented 4 years ago

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.

YairHalberstadt commented 4 years ago

@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.

gerhard17 commented 4 years ago

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)

YairHalberstadt commented 4 years ago

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.

amis92 commented 4 years ago

My opinion: I really like _Imports.razor from ASP.NET Core Razor framework; I'd like to see something similar for C# as well. :)

alrz commented 4 years ago

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

Richiban commented 4 years ago

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.

HaloFour commented 4 years ago

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)
alrz commented 4 years ago

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.

HaloFour commented 4 years ago

@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.

alrz commented 4 years ago

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.

jnm2 commented 4 years ago

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.

HaloFour commented 4 years ago

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.

CyrusNajmabadi commented 4 years ago

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 :)

HaloFour commented 4 years ago

@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.

ericwj commented 4 years ago

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.

munael commented 4 years ago
// MyCommon.cs
namespace MyCommon {
    public using System;
    public using MyTypeA = Quux.MyTybe<A>;
    public using ...;
}

// Foo/Bar.cs
using MyCommon;
// MyTybeA ...
// ...

The above:

  1. Enables all use cases outlined in this proposal (you can put the public usings under an the top namespace)
  2. Enables use cases that inevitably be requested ("global using per folder", for example)
  3. Enables libraries to provide their own "using bundles"
  4. Is accessible and editable with all the tools of an IDE
  5. Seamlessly gels with any future features of using declarations

Personally I don't like the idea of "invisible" usings. 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 usings has so much more potential.

HaloFour commented 4 years ago

@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.

CyrusNajmabadi commented 4 years ago

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).

MgSam commented 4 years ago

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 usings 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.

ritchiecarroll commented 4 years ago

@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.

msedi commented 4 years ago

I think I had some similar discussion a while ago: https://stackoverflow.com/questions/61813506/implicit-assembly-wide-using-directive/61813769#61813769

craigajohnson commented 4 years ago

Take a lesson from VB and please add this. There is only confusion if done wrong.

LeonG-ZA commented 4 years ago

This seems like a real step backwards and archaic. Please don't add this. What problems does this solve?

craigajohnson commented 4 years ago

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 offers me nothing and I have dreams with Ctrl-. / Enter to resolve squigglies

ritchiecarroll commented 4 years ago

@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

LeonG-ZA commented 4 years ago

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.

CleanCodeX commented 4 years ago

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.

jukkahyv commented 4 years ago

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.

theunrepentantgeek commented 4 years ago

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.

CleanCodeX commented 4 years ago

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.

jnm2 commented 4 years ago

I use search and replace regularly. It pairs well with reviewing the diff immediately afterwards.

danielcrabtree commented 4 years ago

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.

HaloFour commented 4 years ago

@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.

jukkahyv commented 4 years ago

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.

HaloFour commented 4 years ago

@jukkahyv

So instead of int id I could have UserID 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.

CyrusNajmabadi commented 4 years ago

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.

danielcrabtree commented 4 years ago

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).

ericwj commented 4 years ago

Let me just chip in with my opinion.

I think the current using would be more readable and useful if

ericwj commented 4 years ago

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.

ericwj commented 4 years ago

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.

LeonG-ZA commented 4 years ago

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.

CleanCodeX commented 4 years ago

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.

hultqvist commented 3 years ago

@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.

https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-01-13.md#file-scoped-namespaces

333fred commented 3 years ago

@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.

https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-01-13.md#file-scoped-namespaces

Hmm? We didn't talk about blazor in any fashion.

hultqvist commented 3 years ago

@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 commented 3 years ago

@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.

alrz commented 3 years ago

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.

theunrepentantgeek commented 3 years ago

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.