-
Notifications
You must be signed in to change notification settings - Fork 4.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: Nested local type declarations #9523
Comments
I guess my question here is what kind of "local" types are we discussing? Full-fledged types which just happen to be declared and scoped within a method block? Anonymous implementations of an interface or parent type? All of the above? What are some specific use-cases? From my experience with Java it seems that anonymous implementations are quite common (not even including functional interfaces). Local classes seem relatively rare by comparison as nested classes seem to be sufficient in isolating code at least from outside consumers. |
Full-fledged types which just happen to be declared and scoped within a method block. I think anonymous implementations are another topic which apparently won't happen (bit sad about that). One use case that I've had in mind is smart tuples, structs that are one step away from anonymous types but that implement an interface or have decision-making ability of their own. Currently you'd have to declare them outside a method even though they rarely get used in more than one method. Another use case is returning a private implementation from a method. Sure, I can simulate this feature with internal (or private in nested classes) using a region, but it would feel so much more clean to scope it more precisely. And declare it near where it is used in the method body. This isn't about external consumers; it's about organizing internals in a cleaner way. |
Ok. In my opinion, the biggest advantage to local types over nested types is the ability to enclose its parent scope. The question is then how to deal with collisions/shadowing with that scope.
The easy answer would be, "do what Java does." I don't know if that would be the correct answer, though. Java's scoping rules for local classes are somewhat complex. |
I see the rules as identical to nested types but with more limited scope:
|
So you don't think that local types should enclose the scope of their parent method? Why should local types be different than any other aspect of C# where lexical scoping is the norm? Without lexical scoping I don't really see much point to local types at all. |
To note, with local types I'd expect the following to work: class A { protected char a = 'a'; }
class B { protected char b = 'b'; }
public class C : A {
private char c = 'c';
public static char d = 'd';
public void CreateLocalObject(char e) {
char f = 'f';
class Local : B {
char g = 'g';
public void PrintVars() {
Console.WriteLine(g); // Local.g
Console.WriteLine(f); // local variable f
Console.WriteLine(e); // local parameter e
Console.WriteLine(d); // static C.d
Console.WriteLine(c); // C.c
Console.WriteLine(b); // B.b
Console.WriteLine(a); // A.a
}
}
Local local = new Local(); // Create an instance of the local class
local.PrintVars(); // and call its printVars() method.
}
} If local types also supported "hoisting" as local functions do I would expect that it would continue to work with |
If you think it's valuable, I won't argue with that. I just can't think of a particular use case for dual What's the mechanism by which each instance of |
The same way local functions and lambdas do, the actual locals/fields/whatever are enclosed and promoted to fields on a state machine that is accessible to both the declaring method and the local type. |
I think one compelling use case for this is to avoid methods like |
@HaloFour Conceptually that makes sense but mechanically, what does |
Possibly. What the compiler does now for lambdas that enclose public void DoStuff() {
string greetings = "Hello";
Action action = () => Console.WriteLine(greetings + " " + this.ToString());
}
// becomes
private void DoStuff_DisplayClass {
public C _this;
public string _greetings;
public void action() {
Console.WriteLine(_greetings + " " + _this.ToString());
}
}
public void DoStuff() {
DoStuff_DisplayClass temp = new DoStuff_DisplayClass();
temp._this = this;
temp._greetings = "Hello";
Action action = temp.action;
} I don't know if local functions did this differently, it was mentioned that they could silently insert As for local types, I do imagine that if you omit a constructor that one would be created which might accept |
@HaloFour Lambdas can only be passed around as delegates (object reference and Imagine the following methods: public static T Create<T>() where T : new()
{
return new T();
}
public static object Create(Type type)
{
return Activator.CreateInstance(type);
} And I tried using them with your Create<Local>();
Create(typeof(Local)); Would that work? If yes, how? If not (because |
That is an excellent point. In Java a local class can never have a parameterless constructor. It always accepts at least the instance of the parent type as an argument, even if it never uses it, as well as other enclosed variables. I don't think that lexical scoping would be possible unless the constructor of the local type can be automatically modified by the compiler to accept the enclosed state in some form. In my opinion lexical scoping is a more valuable feature than being able to use the local type via reflection or constrained generics, and with more use cases. Note that while I think that lexical scoping is important I don't want to hijack this proposal. If simple type scoping is all people really want then so be it. But it is my opinion that the scoping benefits alone aren't enough to warrant such a language change. Every other feature in C# that permits nested declaration enjoys lexical scoping and I think it would be quite confusing if this was the one exception. That said, I wouldn't want to just copy whatever Java does either. |
If we simply don't have a name for the local class, then it's impossible for a user to fall into those confusions. Basically, an anonymous type declaration instead of a local type declaration. In @HaloFour 's example, the |
@HaloFour #13 syntax doesn't make sense to me, it actually does combine declaration and initialization. I'm talking about (Java) anonymous classes. For parsing issue I'd suggest |
@HaloFour While you declaring it? public IEnumerator<T> GetEnumerator() {
return new IEnumerator<T> {
public bool MoveNext() { ... }
public T Current { .. }
}
} |
@alrz What you are describing is exactly the topic of #13. I'd like to differentiate this topic (allowing types to be locally scoped) from that one (anonymous implementation types). We already have anonymous locally scoped types. This issue is specifically asking for named locally scoped types with all the capabilities of nested types. |
I think that scoping(visibility) would be quite useful, but I also agree with @HaloFour that scoping(lexical) is essential. This is very useful in languages like JavaScript and should not be underestimated. Also I think the argument that lexical scoping is the norm in C# combined with the fact that lexical scoping is the best form of scoping there is, is very strong. I think conflicting references within the inner type could be resolved by qualifying them with |
|
Java introduces a lot of extra syntax to deal with the implicit |
I'm on the same page here.
This could be pay-for-play with a smart compiler, but this would mean all ctors would have to be injected invisibly and instantiation outside the method's lexical scope would be impossible. What if you need to construct and pass a nested type into a method from outside that scope? What about reflection, deserialization? Also these things become ctor params. Assuming Btw field injection could work around all these issues but then you'd have to grep even more magic, worrying about where lexically the type was constructed. Let's not go there. |
Exactly, the enclosed values would be passed to a hidden constructor parameter, probably bundled together in a single display reference type. This is the same as with closures today. Or, if the local type doesn't define a constructor, or the local type doesn't use those enclosed values within a constructor, those values could just be fields defined implicitly directly on the local type.
I wouldn't expect that to be possible anyway. A type defined within the scope of a method is not in scope outside of that method. You shouldn't be able to instantiate it or reference it by name anywhere but the method in which it is defined (and any other scopes within that method, e.g. other local types or closures). The actual type name should be compiler generated, mangled and inutterable, just as they are with closure display classes, and just as they are with local functions. If you need a type that can be referenced between multiple methods you already have normal nested types.
Assuming those accessibility modifiers work as expected where? I'd expect that any accessibility modifiers on the nested type itself would be forbidden. The type is implicitly private.
Indeed, let's not. There's no reason to. Handling enclosed lexical scoping through hidden constructor parameters is already established in other languages and works quite well. |
Correct. Not sure what I was thinking there. So... last problem is reflection and serialization (JSON.NET et al). How does a serializer instantiate a nested method type? I can see easily 50% of my use cases involving serialization. |
I imagine that typical reflection scenarios would "just work." The local type should be the same as any other nested type with the exception of some degree of name mangling and possibly any hidden constructor parameters to support closures. I don't think either would impact serialization via JSON.NET. As for deserialization, I assume that this method accepts a |
JSON.NET needs a default ctor. Are you saying a nested type should have a default ctor which calls the invisible injection ctor with default values for the injected params? The net effect would be that when the nested type accesses the method variables, they are default/null if the type was constructed by JSON.NET vs if the type was constructed within the method. That doesn't sound clean to me. |
I'm saying that as long as the local type doesn't enclose over the method's scope then the constructor would be unaffected. But if the local type does enclose over a local or whatever then the local type would no longer have any parameterless constructors and you'd have to be aware of that when trying to use the type with something like JSON.NET. In Java if you declare the following: public void method() {
final String greetings = "Hello!";
class Foo {
void sayHello() {
System.out.println(greetings);
}
}
Foo foo = new Foo();
foo.sayHello();
} It is effectively transformed into: private static class method$1Foo {
private final String greetings;
public method$1Foo(final String greetings) {
this.greetings = greetings;
}
void sayHello() {
System.out.println(greetings);
}
}
public void method() {
final String greetings = "Hello!";
method$1Foo foo = new method$1Foo(greetings);
foo.sayHello();
} |
So the deserialization scenario won't work if you close over a local. Guess that makes sense. I wonder if there is any way to close over the locals by reference so that they can be written as well as read and synced between nested type instances? |
Sure. Java made the decision that enclosing locals from the method scope requires that those locals be public void Method() {
var message = "Hello!";
class Foo {
void SayHello() => Console.WriteLine(message);
}
var foo = new Foo();
foo.SayHello();
} would be translated into something like this: private class MethodFooDisplayClass {
public string message;
}
private class MethodFoo {
private readonly MethodFooDisplayClass locals;
public MethodFoo(MethodFooDisplayClass locals) {
this.locals = locals;
}
void SayHello() {
Console.WriteLine(locals.message);
}
}
public void Method() {
var locals = new MethodFooDisplayClass();
locals.message = "Hello!";
Foo foo = new Foo(locals);
foo.SayHello();
} Although maybe something more clever could be done to avoid the additional allocation for the locals. |
This looks like to be the only open request for something analogous to Java's anonymous inner classes, so +1 for this. I've been running into this a lot where I have a collection of singletons - that is, a collection where each member is an instance of a different subclass of a common abstract class. In Java this is really easy with anonymous inner classes - basically allowing me to do a full class declaration in the body of a method, one that captures references to the surrounding scope, and instantiate it in the inner-class-declaration expression. Extremely useful. Otherwise, you either have to use reflection to populate the singleton-collection by searching through the classes, or you have to define the classes and list separately and maintain redundant code (and somebody will always forget to add an instance of the class to the singleton-collection). |
@gafter Should I move this to csharplang? |
@jnm2 Sure, if you want to keep the discussion moving. |
Issue moved to dotnet/csharplang #130 via ZenHub |
Extend the languages to support the declaration of types in block scope.
As per gafter's instructions. This would be a terrific feature for the same reasons as local functions: scoping, keeping together when moving, declaration closer to the usage.
The text was updated successfully, but these errors were encountered: