I believe this proposal idea -- if implemented -- would require changes in both CoreCLR and the C# compiler, thus I'm posting it in this repo.
Various usage cases exist where it is desirable to extend sealed classes. This is already partially supported by the preexisting "extension methods" feature in C#, but it is rather limited. I propose to further reduce these limits as follows. These limits could be further reduced. I would like to share the following idea with the community, for discussion purposes.
Allow a class/type to derived from a sealed class/type, but with the following limitations and abilities:
virtual
methods in the sealed base class.protected
members in the sealed base class. Alternatively, if this causes a problem for some reason, it would also be acceptable to deny access to protected
members.The following limitation is likely necessary and is acceptable:
Any new interface implemented by the subclass is only available/visible via the subclass, or if the object/instance is specifically typecasted to that interface. For example:
sealed class MyBaseClass
{
}
class MySubClass : MyBaseClass, ITest1
{
}
interface ITest1
{
}
MySubClass instance = new MySubClass();
ITest1 testInterface = instance; // OK, as normal.
MyBaseClass baseInstance = instance;
testInterface = baseInstance as ITest1; // may produce NULL.
To clarify, this message is intended to be only an idea for discussion purposes, not a feature request nor demand. It's not even a suggestion anymore. Apparently I made a mistake when I named this issue with "Proposal:", thus I've now renamed it to "Idea:" in order to be friendlier. To my surprise, I didn't know that non-staff participants of .NET Foundation repos interpret "Proposal:" negatively, approximately as if it means "Feature demand:". It was not my intention to represent this issue or previous issues as feature requests or demands. I hope that the intent is now clear: Only an idea for discussion purposes.
Various usage cases exist where it is desirable to extend sealed classes.
What are those?
@svick
For example, the following demonstrates the standard technique of making a new class that wraps an instance of a sealed class. Although this succeeds, it is an awkward workaround, especially when the sealed base class contains a large number of members that the new class must manually duplicate and forward to the sealed base class. The following example is not especially bad because the sealed base class contains only 3 members to forward, but a real class can contain a much larger quantity of members and then the wrapping/forwarding solution is a tedious awkward workaround.
sealed class MyBaseClass
{
public int Method1(int a, int b, int c, int d) {...}
public int Method2(int a, int b, int c, int d) {...}
public int Method3(int a, int b, int c, int d) {...}
}
class MySubClass_Workaround : ITest1
{
private readonly MyBaseClass _InternalObject = new MyBaseClass();
public int Method1(int a, int b, int c, int d)
{
return _InternalObject.Method1(a, b, c, d);
}
public int Method2(int a, int b, int c, int d)
{
return _InternalObject.Method2(a, b, c, d);
}
public int Method3(int a, int b, int c, int d)
{
return _InternalObject.Method3(a, b, c, d);
}
... and so forth, for each and every member of the sealed base class ...
}
Another disadvantage of the wrapping/forwarding workaround is that it causes a doubling of the number of objects at runtime. This becomes significant when a program needs to create many instances of MySubClass_Workaround
. In contrast, if the base class was not sealed by Microsoft or whoever made it, then the normal manner of making a derived class results in no change to the quantity of objects at runtime.
In https://github.com/microsoft/microsoft-ui-xaml/issues/780 unsealing is described as a "longstanding request from our community", thus the community does view it as a significant problem when they're prevented from making derived classes.
If you want to derive from something sealed you need to talk to the sealing party and get them unseal it for you. Simply deciding that the api provided is not sufficient does not give you the insight needed to know why that decision was made and second guess it. Allowing override of the sealing decision makes sealing useless and potentially exposes previously safe implementation details to misuse which I'd consider a massive breaking change.
@Wraith2 wrote:
Allowing override of the sealing decision makes sealing useless and potentially exposes previously safe implementation details to misuse which I'd consider a massive breaking change.
I agree, but I didn't propose to allow override of the sealing decision. The points you raised are good points but they're not applicable to my proposal.
The action of sealing prevents you from constructing an is-a relationship using the sealed class. allowing even limited derivation breaks that decision.
@Wraith2
The action of sealing prevents you from constructing an is-a relationship using the sealed class.
That's not an immutable fact. That's your opinion of what sealed class
should mean. I disagree with it.
allowing even limited derivation breaks that decision.
The proposal doesn't break a decision, rather it corrects a mistake. Being open to correction of mistakes is a very important and essential part of professional software engineering.
You mentioned "massive breaking change" but you've reversed it. The "massive breaking change" would occur in the reverse direction of what you said. If derivation was originally allowed then later disallowed, then it would be a massive breaking change, but you said the opposite.
exposes previously safe implementation details to misuse
That's incorrect. My proposal stated:
I appreciate constructive criticism -- I really do -- but did you read the entire proposal before criticizing it? It was only a short proposal, but unfortunately your reply sounded like you read only the first paragraph and then quickly posted a knee-jerk rejection.
I would completely understand if you don't have time to read all pages of a 50-page specification. I wouldn't have time to read such a long document. But in this case, my proposal was only half a page.
@svick -- I admire your patience. I don't know how you do it, but somehow you do. That's a strength of yours.
That's not an immutable fact. That's your opinion of what sealed class should mean. I disagree with it.
sealed (C# Reference)
"When applied to a class, the sealed modifier prevents other classes from inheriting from it."
the sealed langauge feature is implemented using the sealed CLI feature.
Standard ECMA-335 - Common Language Infrastructure (CLI)
II.10.1.4 Inheritance attributes,
"sealed specifies that a type shall not have derived classes. All value types shall be sealed."
You'll need to convince people that the runtime spec, runtime implementation(s), language(s), compiler(s) and core libraries should change. This would need the benefits to doing so to be high and very clear and for the compatiblity cost of doing so to either be negligible or the benefits to breaking any existing code to be immense. I feel your proposal will not meet these tests.
@Wraith2 -- That's not an immutable fact. That's your opinion of what sealed class
should mean.
You've linked to the 6th edition (2012) of ECMA-335. That's 7 years old. That document is out-of-date. When the 7th edition is eventually published, it will correct the mistakes in the 6th edition. Standards are not intended to be treated like a bible. Every standard contains defects/deficiences/mistakes.
the benefits to breaking any existing code
My proposal doesn't break existing code. As I said in my previous message, you reversed it.
You'll need to convince people that the runtime spec, runtime implementation(s), language(s), compiler(s) and core libraries should change.
No, no, and no. That's not how it works. It's not my job nor my responsibility to convince anyone.
@svick
For example, the following demonstrates the standard technique of making a new class that wraps an instance of a sealed class. Although this succeeds, it is an awkward workaround, especially when the sealed base class contains a large number of members that the new class must manually duplicate and forward to the sealed base class. The following example is not especially bad because the sealed base class contains only 3 members to forward, but a real class can contain a much larger quantity of members and then the wrapping/forwarding solution is a tedious awkward workaround.sealed class MyBaseClass { public int Method1(int a, int b, int c, int d) {...} public int Method2(int a, int b, int c, int d) {...} public int Method3(int a, int b, int c, int d) {...} } class MySubClass_Workaround : ITest1 { private readonly MyBaseClass _InternalObject = new MyBaseClass(); public int Method1(int a, int b, int c, int d) { return _InternalObject.Method1(a, b, c, d); } public int Method2(int a, int b, int c, int d) { return _InternalObject.Method2(a, b, c, d); } public int Method3(int a, int b, int c, int d) { return _InternalObject.Method3(a, b, c, d); } ... and so forth, for each and every member of the sealed base class ... }
Another disadvantage of the wrapping/forwarding workaround is that it causes a doubling of the number of objects at runtime. This becomes significant when a program needs to create many instances of
MySubClass_Workaround
. In contrast, if the base class was not sealed by Microsoft or whoever made it, then the normal manner of making a derived class results in no change to the quantity of objects at runtime.
I believe this use case will be supported with the C# proposal "Extension everything" A discussion about it in the C# language repo
Although to be fair I kind of lost track if it's still happening in the future or not.
You'll need to convince people that the runtime spec, runtime implementation(s), language(s), compiler(s) and core libraries should change.
No, no, and no. That's not how it works. It's not my job nor my responsibility to convince anyone.
I'm afraid that is how it works. You want this feature. If other people aren't convinced it's valuable, they won't do it. So whether or not it's your job, this feature will simply not happen unless you can pursuade the relevant parties it's worthwhile.
Inheritance must be designed for by the author of the parent type. Otherwise, it's rather easy to break assumptions in the parent type. Now maybe this proposal mitigates that by disallowing everything that could break invariants.
To provide a counterpoint in this discussion: I think everything should be sealed by default. The C# language made a mistake here, and the .NET Framework authors sometimes perpetuate that mistake by enabling misuses of inheritance.
Substitution inheritance is awesome (IEnumerable
, Stream
, ...). Inheriting just to stick another field onto some object is dirty. It's better to wrap/compose. Or, to use something like the WinForms object Control.Tag { get; set; }
feature which is useful and architecturally sound.
Sometimes, inheritance can be a valuable performance win. For example, I understand that await
can stick the state machine on an internal Task
derived type... This is a very nasty hack in my book, but it's worth it. But this "performance hack" form of inheritance should be internal to a library.
As an example, I think all public forms of inheritance on Task
are a design mistake in the framework. Derived classes can do nothing but to stick another field onto this type. There is nothing useful to override.
@YairHalberstadt wrote:
You want this feature.
No, I don't. Apparently I made a mistake when I named this issue. I've now corrected my mistake by renaming this issue, and by modifying the original message to use a friendlier choice of wording.
Here is a fictional description of a scenario that sometimes occurs in real life by accident. Imagine a scenario where a random innocent person -- let's call him "John" -- posts a feature proposal to a .NET Foundation repo/forum. Two or three other participants (let's call them "Richard" and "Harry") are very big fans of that particular .NET repo/project. They are not staff members, but nevertheless they are very vocal and frequently post replies to everyone, and they reply to "John" with such a strong tone of authority that "John" unwittingly starts to respond to them as if they are official spokespeople of the .NET Foundation, despite the fact they are only random members of the public with no higher status than John. Richard and Harry strongly insist that because John wants the new feature, John must make a very big effort to convince and persuade the relevant people to implement the new feature.
After a while, John accepts the authoritative advice as if it came direct from an official spokesperson or employee of the .NET Foundation or Microsoft, because he was persuaded by the strong tone of authority that Richard and Harry habitually use in their favorite .NET Foundation repo/forum. Thus John begins to make a very big effort to persuade "Sally" to implement the new feature that he wants. Sally is the relevant person for that area. Sally carefully considers John's proposal, and after a couple of weeks, she decides to decline it. John doesn't accept Sally's final decision. John tries again and again and again over the course of months to persuade Sally. Sally feels harassed and stressed.
John does not even realize that he is harassing Sally. John thinks he is merely doing what must be done, according to what Richard and Harry insisted he must do. The .NET Foundation's Code of Conduct includes _"a harassment-free experience for everyone"_, but nevertheless Richard and Harry strongly insist on convincing participants to use a process of persuasion that carries the risk of triggering unintentional harassment.
@verelpode
By pursuade, I mean the following:
Present enough evidence and arguments in comments on this issue to pursuade the relevant parties of the benefits and plausibility of your suggested proposal.
It may be acceptable to tag the relevant parties once or twice to indicate your idea and the evidence you've collected. Certainly it not acceptable to troll them.
In no way do I, or as far as I know anyone else here advocate any form of harassment, or even any form of pushing a feature request other than by civil communication on the relevant issue.
All I meant by saying that you will have to pursuade the relevant parties, is stating the fact that no-one else is likely to do the leg work to bring sufficient evidence the feature is worthwhile other than you. If you don't do that leg work than as a matter of fact your feature will be unlikely to be implemented.
Till now noone has ever indicated that they understood me any way other than the way I described. I will try to express my point more clearly in the future.
@YairHalberstadt -- Thank you for sharing your opinion regarding the best proposal process, both now and in the past. My opinion is this: I disagree with your process, but you're free to use your process, and I'm free to use my process. My final decision remains the same as it was the previous time: For myself, I have decided not to adopt your process.
Various usage cases exist where it is desirable to extend sealed classes.
What are those?
Side car-ing data through an api that makes a callback to user code; but only takes the sealed type is probably the main one. Though most BCL libs take unsealed parent classes, even if there is a sealed version, so it doesn't occur that much.
Side car-ing data through an api that makes a callback to user code; but only takes the sealed type is probably the main one.
And even this particular scenario can be solved today with a ConditionalWeakTable<K, V>
.
- The subclass can implement any new interfaces that are NOT implemented in the sealed base class.
- Any new interfaces implemented by the subclass are only available/visible via the subclass (see following example).
- The subclass can define new members (fields, properties, events, methods, constructors).
This will subtly break certain code that uses marshaling, serialization, or reflection. Presumably we could update them to be backward-compatible, but it seems like an error-prone update and I'm not confident we could solve every case in a way that is consistent for the user. This would also require new CLR features which would break backward compatibility with older .NET and Mono versions. These kinds of updates aren't out of question, but we'd want a strong justification for doing so.
- The subclass can access protected members in the sealed base class. Alternatively, if this causes a problem for some reason, it would also be acceptable to deny access to protected members.
This is one of the key reasons for a class to be marked as sealed
, so we won't want this.
Various usage cases exist where it is desirable to extend sealed classes.
Can you provide a killer example where this feature would enable something previously not possible, or where it would significantly reduce effort to implement something?
Also, have you seen Extension Everywhere? This would allow the safe aspects of your idea.
@scalablecory
This is one of the key reasons for a class to be marked as sealed, so we won't want this.
OK. I accept your viewpoint on this subpoint, so let's modify the proposal accordingly:
The subclass can access protected members in the sealed base class. Alternatively, if this causes a problem for some reason, it would also be acceptable to deny access to protected members.
The subclass cannot access protected
members in the sealed base class.
By the way, re "acceptable" in the above quote, that's probably a contributing factor to the strange negative knee-jerk reactions from some forum participants (not you): Those people might have misunderstood my use of the word "acceptable". They might not have understood that I meant "acceptable" as jargon/terminology. Likewise many standards use jargon such as "shall" and "shall not" and it sounds terribly impolite but it's not impolite because "shall" is special jargon in those standards.
I can understand and sympathize when negative emotions are triggered by impolite wording, but in this case "Proposal:", "acceptable", "shall not", etc aren't impolite words, and also aren't demands or orders. Just speaking in general (not about anyone in particular), some people in society have suffered unfair and awful mistreatment in school in the past, and this can trigger hypersensitive reactions later in life (including inability to accept suggestions for corrections to mistakes), but my use of jargon was intended to be respectful, polite, and direct, from one professional to another professional. It was certainly never intended to mean _"I want this feature and you must make it, and only XYZ is acceptable."_
I do NOT want this feature. I want to discuss it. I'm also happy to modify the proposal in response to feedback.
Also, have you seen Extension Everywhere? This would allow the safe aspects of your idea.
Thanks for your helpful link to https://github.com/dotnet/roslyn/issues/11159 and thanks also to @HaraGabi for linking to https://github.com/dotnet/csharplang/issues/164
Can you provide a killer example
sealed class
is good but contains one mistake. That one mistake is that sealed class
is excessively restrictive. These kinds of updates aren't out of question,
These kinds of updates have already been performed multiple times in the past in order to fix mistakes, or to implement improvements or new features. But I'm not going to try to pressure you or anyone else into implementing this feature. As always: If you like the idea, then you're welcome to use it or a modified version of it or whatever you want. If you don't like the idea, then you're also welcome to discard it. Whatever you want; it's your choice. That's how it works. This applies to all proposals from all participants. If you like it, use it. If you don't like it, don't use it.
we'd want a strong justification for doing so.
That's understandable, but other forum participants seem to misunderstand this point. Thus for their benefit not yours, I'll say this: The justification is primarily your responsibility/job, not mine. I understand that if I want something, then _obviously_ the chances are better if I help staff members to do their jobs, such as by researching and providing justifications for proposals. Thus it's helpful if I provide justifications, and I'd like to do this where possible (if time permits), but it's not my job and not my responsibility to do so.
The way you've said "strong justification" is reasonable, but the problem is that some non-staff forum participants frequently copy your wording and misuse or abuse it in many messages in the CoreCLR repo. Your extreme fans want to be you so strongly that they start pretending that they _are_ you. (Indirectly pretending, not outright, and not deliberately.) They start copying your wording, tone of authority, etc. A typical extreme fan tends to unconsciously mislead other forum participants into believing that he (the fan) has a very high status or that he is an official spokesperson of the .NET Foundation or Microsoft, when in reality he is merely just another random participant of the forum with equal status as the other participants.
Extreme fans tend to drive away "normal" (non-extreme) participants of forums/repos. The demographic of the repo changes to mainly a group of co-dependent extreme fans, and many or most of the "normal" participants are driven away. "Normal" participants typically don't have the very large amount of patience necessary to deal with extreme fans.
Extreme fans aren't bad people. They're actually good people who do bad things without knowing it.
Many .NET Foundation forum participants are fans of @MadsTorgersen; he's their idol; so let's learn something wise from Mads. Normally Mads does not give "Thumbs Down" :-1: to any message without posting an explanation of his reasons. Why? Three reasons:
At the time of writing this message, approx 10 people have given "Thumbs Down" :-1: in this issue, without posting anything to explain their opinion or reasons. Not even one single sentence; just a "Thumbs Down" alone! This usage of "Thumbs Down" is actually impolite and fits poorly with the Code of Conduct. A very good reason exists to explain why it is impolite. It is impolite because it is approximately equivalent to saying something along the lines of:
"I'm so important and my status is so extremely high that I don't need to bother explaining anything. Simply my disapproval alone is sufficient. My opinion has large weight regardless of whether I state my reasons or not. I'm like a king and kings don't have to explain their reasons."
Although it's quite bad to behave similar to the above, people who use "Thumbs Down" impolitely are still good people -- their bad behavior is unintentional. It's all an accidental misunderstanding. It's not their fault. It's the fault of the schools and universities: Many schools and universities fail to teach the essential topics of life and work. Many assign extraordinarily high priority to low-priority topics and ignore the essentials, and they unethically force students against their will to learn these low-priority topics instead of the essentials. Thus I don't blame people for using "Thumbs Down" in an impolite manner.
- In microsoft/microsoft-ui-xaml#780 unsealing is described as a "longstanding request from our community", thus the community does view it as a significant problem when they're prevented from making derived classes.
Unfortunately, I don't think that issue really helps sell your point. That's focused on one library/framework, where it was believed that the general design of the library was too restrictive. As that issue points out, it's not just a matter of removing sealed
- you want to do some other work to make things behave properly (this is the justification for the rule "design for inheritance, or prohibit it"). Even then, the issue points out they may not want to unseal everything. The general advice of "you need to tweak your design" isn't invalidated here.
However, from what I've read of your proposal so far, _none of the things you've proposed would actually help users of that library_. You can't override existing methods, or otherwise change any existing behavior - the library would have to behave exactly the same (ExtensionEverything is also in the same boat - it doesn't provide new behavior to existing libraries).
@GSPP wrote:
It's better to wrap/compose.
Thanks for your legitimate critique. I have to disagree but I still appreciated your critique anyway. The reason I disagree is because of the awkwardness and ongoing maintenance difficulty of manually forwarding a large quantity of members. For more info, see the example code in my earlier reply to @svick.
@Clockwork-Muse wrote:
from what I've read of your proposal so far, none of the things you've proposed would actually help users of that library. You can't override existing methods,
Thank you too for your legitimate critique. I think you're raising an important point, and this could mean that the current version of my idea/proposal is still excessively restrictive. I mentioned that sealed class
is excessively restrictive. This means, if my idea/proposal is still too restrictive, then the proposal fails to adequately solve the problem of sealed class
being excessively restrictive. Therefore maybe the proposal should be modified to further reduce the restrictions of sealed class
, or otherwise brainstorm about potential solutions.
@Clockwork-Muse -- I just thought of a reason why your good point may have to be modified as follows:
none of the things you've proposed would actually help users of that library. You can't override existing methods
_The things you've proposed would not help ALL users of that library, but it would help a certain percentage of them. You can't override existing methods, but you can subscribe to events, and implement new interfaces, and add useful new properties, etc._
The following example demonstrates how the problem would indeed be solved for X% of the library users, but not all users, depending on their particular needs. The following class doesn't override any virtual
members, but nevertheless succeeds in achieving the goal of making a derived class of TextBlock
(sealed) that auto-shrinks the font size when the TextBlock
is resized to a smaller size. Furthermore, it also succeeds in its goal of adding support for a fictional interface IConvertibleToRichText
that allows objects or UI elements to be converted to RichEditTextDocument
.
class MyExtendedTextBlock : Windows.UI.Xaml.Controls.TextBlock, IConvertibleToRichText
{
public MyTextBlock()
{
// Subscribe to public event "SizeChanged" defined in a base class:
base.SizeChanged += this.OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.IsAutoShrink)
base.FontSize = XXXXX;
}
public bool IsAutoShrink { get { ... } set { ... } }
RichEditTextDocument IConvertibleToRichText.ConvertToRichText()
{
return XXXXX;
}
}
Even with this new knowledge, my previous reply is still valid: I still think @Clockwork-Muse raised an important point (even if not 100% accurate), and it's still worthwhile to consider further reducing the restrictions, further than the current version of my proposal. Currently the proposal does solve the problem for X% of the library users, but if the restrictions are even further reduced (if possible), then this X% can be further increased.
@scalablecory wrote:
Presumably we could update them to be backward-compatible, but it seems like an error-prone update and I'm not confident we could solve every case ...
You're in a better position than I to estimate the amount of work required to implement the fix for sealed class
, but here is an idea that could reduce the difficulty of the work, and improve the reliability. When a sealed class such as the following is encountered...
class MyBaseClass
{
public virtual void Method1() { }
public virtual void Method2() { }
public virtual void Method3() { }
}
sealed class MySealedDerivedClass : MyBaseClass
{
public override void Method1() { }
public override void Method2() { }
public override void Method3() { }
}
...treat it / interpret it as a shortcut that means exactly the same as:
class MySealedDerivedClass : MyBaseClass
{
public sealed override void Method1() { }
public sealed override void Method2() { }
public sealed override void Method3() { }
}
Thus sealed class
would be interpreted as a shortcut way of specifying that every member of the class is sealed. Either way, whether sealed
is specified at the class/type level or at the member level, it would behave the same.
I suspect that this technique would eliminate the "error-prone update" problem that you mentioned, BUT my guess could easily be wrong -- the internal details of CoreCLR is not my area of expertise. So I'll defer to your opinion regarding whether or not this technique makes the work easier.
@verelpode
Thank for your suggestion.
Various usage cases exist where it is desirable to extend sealed classes. This is already partially supported by the preexisting "extension methods" feature in C#, but it is rather limited.
I think you're onto something here; in the past we have proposed "extension everything" which we recently discussed in the C# language design and concluded that this isn't the direction we feel worth pursuing because even in that world, expressiveness and composability would still be limited and the cost for the feature would be quite high.
Instead, if we're doing something, we're likely going to do something more useful (e.g. protocols, traits...).
Quite frankly, I feel like your suggestion here is also somewhat fighting the symptoms more than actually advancing expressiveness/extensibility.
Subclassing, while certainly useful, doesn't always work because you don't always control the instantiation. Extension methods don't suffer from that but it also means you can't override existing members and modifying behavior.
Futhermore, even if we were to relax sealed
types in the way you propose you very often still wouldn't be able to derive because in many cases the type doesn't expose the right public constructors or even any constructors.
A few reason why the relaxation wouldn't work for us:
Today, sealed means the runtime can safely assume that throughout the lifetime of the process this type cannot have any derived types. This allows for optimizations that otherwise would be hard/impossible to do. This also means relaxing the rules will likely have a bug tail through the ecosystem of tools (compilers, runtime, JIT, GC, profilers etc). The cost is likely higher than you think.
The subclass can implement any new interfaces that are NOT implemented in the sealed base class.
This particular property cannot be safely enforced over time. Consider if V1 of the platform ships as follows:
```C#
public sealed class Foo : IFoo
{
...
}
You'd now be able to derive as follows:
```C#
class MyBar : Foo, ISomethingElse
{
...
}
Now assume in V2 the platform starts implementing ISomethingElse
on Foo
. Your invariant no longer holds.
The reason why we seal types is precisely to limit extension points so that we have more degrees of freedom to evolve the platform without breaking upstream consumers.
And lastly, your example is about WinRT. In that case, sealed
isn't a .NET type system constraint, it's a WinRT/COM constraint. The WinRT team has mentioned before that they could make their system less rigid and allow more classes to be unsealed, but this would be an unrelated work item. However, this might be your best angle to address your immediate needs.
Because of all of this, I'm afraid I don't see a future where we'd follow your suggestion, which is why I'm closing this.
@terrajobst
Because of all of this, I'm afraid I don't see a future where we'd follow your suggestion, which is why I'm closing this.
Alright. Thanks for considering the idea, and especially thanks for your detailed answer -- I found several of your points quite interesting from a software engineering perspective. I plan to re-read your message again tomorrow, for my own interest in the topic.
Please reopen this issue. Currently it's impossible to implement the Observer Pattern on sealed objects. Which means that it's impossible to implement Reactive Programming on sealed objects. This excludes a large swath of modern programming practices in C# because the sealed
keyword is overused, sometimes due to valid concerns, but often due to rumors of "best practices" or other cargo culting, especially by inexperienced developers.
We either need an unseal
keyword or to deprecate the sealed
keyword.
I'm interested in furthering this cause in other ways, perhaps via petition or contributing to the C# runtime, if anyone has any suggestions.
This is more of a formal proof than an issue. Here is some background:
I'm learning Unity 3D and just hit a blocker due to the C# sealed
keyword. Many of its classes like Mesh
are sealed:
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Graphics/Mesh.cs#L19
I was writing a plugin as a script that could be attached to a game object that would override the way that Meshes are drawn. In order to do that, I need to run a function that updates metadata if a mesh's vertices change. This is in order to avoid the use of geometry shaders on old or mobile hardware. I was going to inherit from the Mesh
class and override the setters so that they trigger an INotifyPropertyChanged callback if any of the vertices, triangles or other arrays change within the mesh.
That way during the object's Start()
phase, I could set the object's MeshFilter.mesh to be my child class and detect if any changes are made to the object's mesh. That way my plugin script could update its metadata automatically, rather than relying on the developer to manually alert me anytime that mesh data is changed (which isn't future-proof, because it's inevitable that some developer down the road will forget to do it).
On top of that, the sealed
keyword is merely security theater, because internally the runtime still maintains the v table for the object's methods (I'm borrowing from C++ terminology here, apologies if it's not an exact description). It's possible to swap out the method's function pointer at runtime, but that requires the unsafe
keyword:
https://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method
I've investigated every way to possibly to do this on loaded code at runtime and have come up short. Some keywords: Castle Dynamic Proxy, Harmony, Cecil, PostSharp.
I think that the sealed
keyword is an anti-pattern because it breaks object-oriented inheritance. Microsoft probably used it as a way to avoid the troubles with name mangling on libraries when class implementations or versions change (as encountered in C++). In other words, the concept of preventing inheritance as a way of enforcing has-a
rather than is-a
design patterns in developer teams was conflated with the concept of freezing an interface (something like the final
keyword in Java).
I've lost (conservatively) two weeks to this. I think the friction caused by the sealed
keyword far outweighs any perceived benefit. I tend to use abstractions derived from first principles rather than patterns, but for those in the latter camp who disagree with me, Martin Fowler has this to say about it:
https://martinfowler.com/bliki/Seal.html
I think that the "directing attitude" he mentions has created a culture where developers feel stymied by decisions outside their control. This causes inordinate hand waving and spinning to over-engineer unnecessary solutions in order to satisfy someone else's definition of what programming should be. Being opinionated like this should be frowned upon in a programming language IMHO.
With all due respect, but I don't think you understand why we're sealing types and why unsealing them won't help you. A few things:
I was writing a plugin as a script that could be attached to a game object that would override the way that Meshes are drawn. In order to do that, I need to run a function that updates metadata if a mesh's vertices change.
Even if C#/runtime would allow you to derive from Mesh
, you wouldn't be able to override any setters as no members on the type you referenced are virtual. You could hide them by introducing new properties but that means any code compiled against the base type would bypass your getters/setters, thus breaking your code.
On top of that, the
sealed
keyword is merely security theater
Sealing has nothing to do with security1. We seal to ensure consumers don't accidentally break invariants. Also, we seal in order to be able to evolve the platform without breaking people. Every extension point reduces the ability for the platform to evolve.
Martin Fowler has this to say about it:
https://martinfowler.com/bliki/Seal.html
I think that the "directing attitude" he mentions has created a culture where developers feel stymied by decisions outside their control.
As someone who works on a platform that is used by millions of people and has billions of lines of code written against it: this doesn't work. If we were to make every type unsealed and and every method virtual, we wouldn't be able to evolve the platform any longer without breaking at least one popular library. Extensibility has to be designed, if anybody can tweak anything, the resulting code is very fragile.
1It used to when we did sandboxing on .NET Framework. We're no longer supporting code access security (CAS) or attempts to sandbox within the process using the .NET runtime.
@zmorris wrote:
I'm interested in furthering this cause in other ways, perhaps via petition or contributing to the C# runtime, if anyone has any suggestions.
Even though I opened this issue and think that the current behavior of sealed class
isn't 100% correct, personally I still wouldn't participate in any "cause" nor petition against sealed class
, because I feel that the .NET Foundation already suffers too much from various "causes" in one form or another, and I don't want the Foundation to suffer, therefore I wouldn't want to start or encourage yet another cause. In general, human nature has the tendency to begin with a cause and then eventually end with something bad such as borderline harassment or even outright extremism or fanaticism. That's why I don't participate in causes.
See, this is another example of the problem that I mentioned. Battles and causes etc are more likely to be triggered (from one side or another) when extreme fans and extreme theorists run amok. The out-of-control extremity in one faction triggers extremity in another faction that was previously non-extreme, and so forth, and the situation deteriorates more and more over time. When extreme fans copy the tone of authority and wording of staff members, and when nobody stops them from doing that, or when silence from staff members gives them credibility, then it tends to eventually lead to derailment of a project, forum, or repo.
Nevertheless thanks @zmorris for the interesting link to Martin Fowler (I found his descriptions of "DirectingAttitude" and "EnablingAttitude" to be insightful and observant). However I don't request that this issue be re-opened, and I don't think a petition is the best way in this case.
@verelpode
Thanks for the thoughtful response. I agree that sealing everything isn't the right answer. FWIW, the FDG state that one shouldn't seal classes unless there is a reason to and while I tend to disagree with that guidance the result is that most types aren't sealed today.
@terrajobst wrote:
I was writing a plugin as a script that could be attached to a game object that would override the way that Meshes are drawn. In order to do that, I need to run a function that updates metadata if a mesh's vertices change.
Even if C#/runtime would allow you to derive from
Mesh
, you wouldn't be able to override any setters as no members on the type you referenced are virtual. You could hide them by introducing new properties but that means any code compiled against the base type would bypass your getters/setters, thus breaking your code.
Thanks for pointing that out. With all my consternation over the sealed
keyword, I overlooked that the Mesh
class methods aren't virtual anyway. Which means (as you pointed out) that any compiled library code would use the base class methods instead of ones I might have overloaded to implement INotifyPropertyChanged
. Which defeats the whole purpose of what I was trying to do, regardless of the sealed
keyword.
I tend to agree with your points as they stand now within the C# ecosystem. However, for posterity I wish to clarify where I was coming from, with some background from other languages:
Conceptual correctness should take priority over implementation details. That's the primary reason that I think that the sealed
keyword was a copout to mask some of the polymorphism complexities encountered by languages like C++ (such as the lack of a library name mangling convention). More work could have been done originally to ensure extensibility, even when serializing classes. Probably some kind of hash of the classes would need to be maintained, a bit like how Java gives every class implementation what basically amounts to a unique UUID, I think I'm thinking of serialVersionUID
: https://stackoverflow.com/questions/285793/what-is-a-serialversionuid-and-why-should-i-use-it this may very well be an open problem, but I'd like to see the big tech companies put more research dollars towards solving these puzzles rather than having thousands of developers each reinvent the wheel.
I'm also personally against the virtual
keyword because I think that all class methods should be virtual by default. I wrote up a draft response to this thread comparing C# and C++ method overriding but realized that C++ also hides non-virtual base class methods if a child class overrides them. I had never noticed, because I so rarely used the virtual
keyword in C++ that for some reason I thought that virtual
was the default! This becomes an issue where you said it does: when compiled libraries are included with a project. If virtual
was the default, then we could override class method behaviors globally in our applications and the libraries would know to automagically run the overridden child class methods by way of polymorphism. Instead, we tend to get stuck with canned library code that misses out on emergent behavior and use cases. Scripting languages like PHP often lean more towards the enabling attitude in this regard:
https://stackoverflow.com/questions/1349637/correct-implementation-of-virtual-functions-in-php
https://www.php.net/manual/en/language.oop5.final.php
So in PHP, class methods are virtual
by default but can be marked final
similarly to in C# or Java. It probably dodges compiled code interoperability issues at runtime by way of its JIT compiling. Personally, I would have voted against the final
keyword in PHP as well, and I have never used it.
Regardless of all of this, the sealed
keyword breaks OO inheritance. Full stop. Now, we can argue about whether a designer or a user has more clout to decide what will be done with the designer's library. And you bring up valid points about being able to evolve the C# platform by ensuring that certain foundation interfaces never change. But arbitrarily saying that a class can't be inherited from another class just doesn't make sense to me, I'm sorry. If there is:
No formal proof that sealed
is the only way to maintain compiled code interoperability (serialization, invariants, etc).
Sealed
is not done for security reasons.
Sealed
can (and does) cause unintended consequences when conceptual correctness in a project is damaged by being forced to use the has-a
instead of is-a
architecture.
Then I'm not seeing a strong argument for keeping the sealed
keyword. And I'm especially not seeing a strong argument for denying us an unsealed
keyword within our own application spaces (which may break versioning, serialization, etc and require additional complexity in the C# runtime I realize).
The gist of my argument is that sealed
saved Microsoft some thousands of dollars originally. It's probably saved a few million dollars by simplifying the rollout of the foundation code used by millions of people and simplifying evolving the platform as you said. But there is also an opportunity cost of untold millions of dollars in preventing the emergent behavior I mentioned, because we'll never know what might have been if people could have extended certain classes (in Unity 3D alone, as just one example) or if C# was more virtual
so users could easily extend compiled library functionality.
I learned functional and reactive programming relatively late in my career, but I much prefer it to the kind of "bare hands" boilerplate-oriented imperative programming popularized by Java and largely copied by C#. Here's why that matters:
Since I'm denied the straightforward ability to inherit Mesh
and implement INotifyPropertyChanged
around the base class methods so that my renderer can observe Mesh
array buffers changing, now I have to manually:
Wrap Mesh
in something like a MyMesh
dynamic proxy containing a MyMesh.mesh
protected member variable.
Reimplement all the setters and any methods that update Mesh
array buffers.
Hand roll INotifyPropertyChanged
(it would be better if there was a way to attach it to an allocated class instance in order to observe its changes).
Implement implicit cast to convert MyMesh
to Mesh
when I need to pass it to third-party Mesh
handling code.
Maybe implement the Transform.hasChanged pattern on Mesh
, and remember to set the Mesh.hasChanged
flag EVERY time I pass my MyMesh
class to a library that may change it (which is self-evidently not future-proof). Adding setters via extension methods is not supported but I might be able to add a meta- property at runtime of Mesh.hasChanged
.
Probably implement a DEBUG phase utility function to watch for changes to MyMesh.mesh
buffers between rendering passes and tell me "you should have set Mesh.hasChanged()" if they differ unexpectedly from the Mesh.hasChanged
state.
Probably implement unit tests for sanity checking all of this (arguably needed anyway).
This is all so very Java-esque. It runs the danger of making C# a toy language and almost certainly guarantees code bloat and over-engineering because no matter how hard I try here, it's impossible to make this future-proof. A single missed Mesh.hasChanged = true
and I could have a difficult-to-reproduce bug in a deployed application. To me, this is as serious as lacking atomic intrinsics for implementing compare-and-swap (CAS). There simply is no viable workaround to do what I need to do, and that really bothers me, especially in a compiled language.
Maybe I should take this up with Unity 3D and ask that any sealed
class at the very least provide an interface and a way to cast classes implementing that interface to the original. Maybe I should write a blog post discouraging direct use of all sealed
classes, and encourage thin wrappers over them instead. Maybe I could even write tools to audit code and detect these direct usages. The rabbit hole goes deep indeed.
Maybe I'm just looking at this wrong. I'm open to suggestions!
I'll lay off of any petitions or further belligerence at this time. Thanks everyone for your help.
The Unity developers aren't stupid, and the sealed
keyword is not the default. If unity chose to mark Mesh
as sealed, and not to provide an interface, I am absolutely certain that that is because of a very good reason.
I think the first thing to do for the purposes of this discussion is simply to find out why. That would greatly inform what a better solution to that problem would be.
@zmorris
I think that all class methods should be virtual by default.
When I opened this issue, I was trying to find a reasonably balanced solution, without springing to any extreme. Isn't an _"Everything virtual by default"_ proposal an extreme or radical proposal? How difficult would it be to convince at least 3/4 of C# software developers to support an "Everything Virtual" proposal? Isn't that almost as difficult as sending a man to the moon?
Imagine you had enough money to hire 50 staff members to work full-time exclusively on the "Everything Virtual" proposal for 3 years. At the end of those 3 years, would the team of 50 be successful in convincing at least 3/4 of C# software developers to support the proposal? I suspect you'd need to double it to 100 staff members and even then success would be very uncertain. Thus it's an extreme proposal, isn't it?
When I opened this issue, I was trying to find a reasonably balanced solution, without springing to any extreme. Isn't an _"Everything virtual by default"_ proposal an extreme or radical proposal? How difficult would it be to convince at least 3/4 of C# software developers to support an "Everything Virtual" proposal? Isn't that almost as difficult as sending a man to the moon?
It doesn't matter IMHO. It would be a massive breaking change. For a platform with millions of developers and billions of lines of code discussions about "refactoring" core design pivots isn't useful because we won't ever be able to get the ecosystem to absorb such changes.
Thanks for your thoughts everyone. I still haven't found a solution, so I've opened a thread on the Unity forums:
https://forum.unity.com/threads/detect-changes-to-sealed-class-like-mesh-reactively.759248/
Most helpful comment
With all due respect, but I don't think you understand why we're sealing types and why unsealing them won't help you. A few things:
Even if C#/runtime would allow you to derive from
Mesh
, you wouldn't be able to override any setters as no members on the type you referenced are virtual. You could hide them by introducing new properties but that means any code compiled against the base type would bypass your getters/setters, thus breaking your code.Sealing has nothing to do with security1. We seal to ensure consumers don't accidentally break invariants. Also, we seal in order to be able to evolve the platform without breaking people. Every extension point reduces the ability for the platform to evolve.
As someone who works on a platform that is used by millions of people and has billions of lines of code written against it: this doesn't work. If we were to make every type unsealed and and every method virtual, we wouldn't be able to evolve the platform any longer without breaking at least one popular library. Extensibility has to be designed, if anybody can tweak anything, the resulting code is very fragile.
1It used to when we did sandboxing on .NET Framework. We're no longer supporting code access security (CAS) or attempts to sandbox within the process using the .NET runtime.