-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal]: file local types #5529
Comments
Is a new accessibility modifier necessary or could the compiler be "relaxed" so that |
What if you're generating part of a partial type? |
Ah, I see, so this could also apply to a nested class emitted within the generated portion of a partial class, where To play devil's advocate, would it not be sufficient to have the compiler recognize an attribute to enforce this behavior? |
The idea of having file private types / methods is a mechanism I use a lot when coding in F#. There you can use a signature / implementation file pairing. Only types / methods that are present in the signature file are available to the rest of the code in the assembly. The rest are only visible to the code within the implementation file. I've found this to be very advantageous when coding in F# and I make use of it often. It's a way of defining helper types that just don't have any visibility within the rest of the assembly. It's a clean separation and really allows you to avoid spaghetti code in larger apps and instead force communication across defined boundaries. I mention F# here because it's the language I've used this feature the most in. But it's present in other languages like Go via exported / non-exported types in packages. I've often longed for a similar feature in C#. When @stephentoub was chatting with me about the problems they are hitting isolating their generator code it occurred to me that this would also be a mechanism for generators to effectively isolate themselves. It gives them a greater degree of freedom in what they generate without the fear that consumers will depend on implementation details that generators never intended to expose. |
Split into files has traditionally (except file-scoped namespaces) been neglectable in C#. Wouldn't it be better to make the types not file-private, but partial class part private? Other usecases seem to be covered by private inner classes. |
@vladd I don't think that would work well. Consider code like the following (using the partial class Server
{
[RegexGenerator(@"(?:[0-9]{1,3}\.){3}[0-9]{1,3}", RegexOptions.IgnoreCase)]
public partial Regex CreateIpAddressRegex();
}
partial class User
{
[RegexGenerator("(.+)@(.+)")]
public partial Regex CreateEmailRegex();
} If the generated code for these two methods wanted to share some helper code, there is no way to use partial class part private types or private inner types. But file private types make it very easy. |
Is it possible to hide the generated code as nested private types inside a special class? I was going to say it's an easy workaround but thinking about it more, it actually looks like a solution, if not the solution. This "container" class can be made partial, hence allowing the generation of a significantly large amount of "private" code without creating enormous files. In abstract terms (not |
Good point. How about an attribute like |
@TahirAhmadov That name, though |
I would be amenable to this idea. My personal take is that the symbols not be available outside the file ever (no attributes to expose it), and that they likely be implemented with a name mangling strategy to avoid cross file collisions. |
The cross file symbol collision is definitely a consideration to take into account. It is a bit of a sore spot in other languages. IIRC F# gives a bit of a cryptic error when you have collisions instead of resolving them. I think we'd likely want to keep the simple names of the symbols the same. For example namespace Example {
file private class Widget { ... }
} Think the identifier should still be Instead think we should consider inserting a generated name between the namespace and the type name. In this case having |
@jaredpar . I think that makes a lot of sense. I agree with you that there is a strong desire to likely have the metadata name match (though it's not sacrosanct for me). Your approach seems like a good best-of-both worlds option. For things like Just trying to think through corner cases here. Thoughts? |
That is a good point about fields. In my head I'd been focusing on the cases where the only type itself was marked as I think at that level of granularity it gets really hard to maintain both:
Imagine for example reflection. The moment we start name mangling individual members then it becomes really hard to think about how to use features like reflection to use them. Thinking along the lines of corner cases, I'm still trying to decide if // file1 .cs
file private partial class Widget {
public overrides string ToString() => "";
}
// file2.cs
partial class Widget {
// Error: multiple overrides of ToString
public overrides string ToString() => "something"; Part of my brain is screaming "only allow |
Can't we use a modreq for this? Something like |
@RikkiGibson you can't put a A That does pose a problem though: such methods could not be used as implicit implementations of We'd still need solutions for The more we talk about this, and it's fantastic feedback, the more I'm wondering if forcing this at the type level is the best solution. It solves the core problems here, allows us to maintain the idea of it being truly private (don't worry abuot name conflicts) and no other language I'm aware of allowed parts of types to be |
I'm not really sure if this is actually needed (at least in the proposed way)? My first thought for making helper types unlikely to be used by non-generator code would simply be a nested namespace. namespace Whatever.YourRegular.CodeUses
{
partial class TypeToExtend
{
partial void DoTheThing() => DontUseThis.Helper.DoTheSharedThing();
// or, if you dislike putting the namespace in front:
//using DontUseThis;
//partial void DoTheThing() => Helper.DoTheSharedThing();
}
namespace DontUseThis
{
internal static class Helper
{
public static void DoTheSharedThing() => throw null;
}
}
} This doesn't prevent non-generator code from using it, but it is more obvious if anyone does attempt to use it, and it doesn't show up by default (since it isn't in the same namespace and you'd have to be I do wonder if I could make use of the proposed solution anywhere (outside of source generators). I can't really think of any other situation where this is both a useful addition and something that can't be done otherwise. I could see a top-level |
And that's the problem. Then we change the generator, and code breaks. |
Can a namespace be file private? C++ uses anonymous namespaces for this purpose:
|
Namespaces don't really exist, they're just part of the class name so they can't have their own metadata like accessibility levels. The same namespace can be used freely between different assemblies as well. The individual types within the namespace would need to carry that metadata. |
@HaloFour |
Different languages, different problems :) Implicitly treating a |
@HaloFour |
I don't think that'd work for nested classes unless C# allowed you to define namespaces within a class. |
I feel like anonymous namespaces are borderline trivia in C++. It's not really obvious what they do unless you already know whereas something like As an aside, personally I've found it to be valuable for generated source to be digestible by humans for debugging/learning purposes. I worry that this feature as proposed could end up forcing generator authors to put their entire output into one file (potentially making it painful for humans to interact with.) It has its own downsides, but I wonder if friend types (#2073) would be a better solution to this issue. (As-is, this whole proposal is basically a less flexible special-case of them.) |
Friends, let me share (may be) unpopular opinion: please do not complicate C# for minor reasons. For SG-produced code the EditorBrowsable approach is enough, we need only make a final design and implement it. Also, @vladd 's proposal about unspeakable namespaces is better because of its lesser impact on the language (as I see it). if we There is a lot of choices, please do not make C# more complex for the minor reason like hiding SG code. Thanks! |
There has been many other proposals, especially related to member visibility, that has been rejected due to
Can anyone here clearly explain why this proposal is not rejected for the same reason? We have been forced to use dirty attributes everywhere to disallow specific usage, and many of there are related with source generators. I don't think hiding something is anything different. |
I would rather have private types only visible to the same assembly, but |
As an idea to define "file-private" types I would suggest unnamed namespaces: namespace Foo.Bar {
internal partial class UserType {
internal partial void SomePartialMethod() {
My.Own.Stuff.Zoo.Blah(); // OK only in current file
new Baz(); // OK only in current file
}
}
}
namespace { // Unnamed namespace declaration, all internal types are "file-private"
namespace My.Own.Stuff { // Named namespace inside unnamed, if needed
class Zoo { // No access modifier, it is required. Or only optional "private" / "internal" allowed
public static void Blah() { }
}
}
class Baz { } // "Top-level" "file-private" enum
} |
I got better idea for file private, and that is to allow |
@AraHaan that wouldn't solve a core case where you want the type hidden from code in the same assembly. A private class would be visible to siblings (just as a private nested type is visible to it's siblings). |
@ViIvanov I like that, but it doesn't provide a solution for if you want only a nested type to be file-private. |
@jnm2, Thank you. Sorry, I've missed this case. |
Will this make it possible to create private extension methods? I was in a situation a couple of times when something might be nicely expressed as an extension method, but that would be too specific for exposing as a method visible for the entire assembly. Top-level private class would solve that problem. |
I would expect to be able to define extension methods in a 'file' class and only be able to use them in the same file. That's a great scenario to test, thanks. |
I don't wanna be the party pooper, but, what's wrong with nested types? If you are writing a source generator, and you don't want the generated types to be visible to the main assembly, just make all the generated sources be part of a big partial class, and the generated types as nested private types in that class. In other words, that big class is just acting as a namespace for all purposes. Even source compatibility is preserved, you're just transforming a namespace into a class that spans multiple files. The fully-qualified names of all the nested types will remain the same. This should definitely be mentioned in the "Alternatives" section of the proposal, after all, it's a "Do Nothing" alternative. |
"Do nothing" is always an implied alternative. When we decide we want to do something, it's because we feel that the value has already proven itself over "do nothing" :) |
This came up as an option. We felt that this wasn't ideal and that defining something just for this purpose is unpleasant. We debated it but ended up here. |
There's no way to hide it completely. If the source generator is filling in a partial method on the user's type, that method needs access to the generated code; whatever it has access to, so will any other user code in the type containing that partial method. The only thing it can completely hide is anything it can generate inside the method itself, and without local types, there's no way to hide such types (and even if it had local types, there'd be no way to share that generated code across multiple partial method implementations without exposing it to the containing type's user code). |
This was implemented in C# 11 right? https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/file. Can this issue be closed? |
@333fred, we keep these open until they're spec'd, yes? Or is that tracked elsewhere? |
That's correct, but thank you for reminding me to label all the 11 issues appropriately. |
This comment was marked as off-topic.
This comment was marked as off-topic.
@Xyncgas if that is the case, then you can safely skip using this feature without issues. |
@333fred: Could the check marks be added for prototype and implemented? I understand not closing until there is a documented spec, but I look for the check marks to know what stage it’s in |
Done. |
Consider 'file' not only being useful for type definitions. In the scenario of "code generation" or/and "partial types" the file keyword is "useful on any member". In my opinion file should be possible on any member as well (fields, properties, events, methods ...). |
I found that issue that the IDE suggests me to convert some |
Absolutely agree. We just need to come to a consensus for how I would personally like us to be able to "just see When it comes to accessibility checks, |
"file private" visibility
Design meetings
The text was updated successfully, but these errors were encountered: