dotnet / csharplang

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

Allow struct inheritance #936

Closed MkazemAkhgary closed 6 years ago

MkazemAkhgary commented 6 years ago

Please allow very basic inheritance for structs.

I know why its not allowed. Because structs have fixed size but inherited struct could have different size.

https://stackoverflow.com/q/1222935/4767498

Now my proposal is to allow very basic inheritance (and maybe polymorphism to some degree) for structs. With one simple rule

derived structs are only allowed to declare methods and other functionalities that dont incur change in size of struct. polymorphism should not be allowed for properties in structs, but sealed properties can be allowed in base class.

I really need this, please dont rush on this one and complain about violating rules, please dont kill creativity, but if you have strong reason why this should not be allowed, i appreciate to know.

I need this for performance reason. I have parent class with 4 childs. Class is light (only 8 bytes and a reference) and its immutable. Size of all childs are also equal, childs dont introduce extra size.

Even if this requires clr changes, im eager to propose that.

yaakov-h commented 6 years ago

What's the point of a (sub-class) struct that does not declare any stored members? All you have then is a collection of functions, which can be static and/or extension methods on the original struct.

MkazemAkhgary commented 6 years ago

First of all you can have generic derived structs restricted to specific types (also constraints play rule in here).

A subclass does not necessarily introduce new field. And it does not defeat purpose of having derived class with extra functionality over existing fields, you could take advantage of polymorphism even in structs as long as they remain fixed size.

There is even point for interface markers, if you didnt read please read answer to this post, it will render your question inappropriate. https://stackoverflow.com/q/1023068/4767498

Also i dont want to reduce my proposal to specific purpose, though i had an specific purpose but this proposal is general.

So there is no single point in this proposal. @yaakov-h

MkazemAkhgary commented 6 years ago

Also collection of static functions is way different than inheritance and polymorphism.

MkazemAkhgary commented 6 years ago

Another point, where you use interfaces to simulate inheritance for structs. Boxing of structs could be avoided if you had base struct rather than interface.@yaakov-h

orthoxerox commented 6 years ago

@MkazemAkhgary

What about using a type tag and arranging the fields using LayoutKind.Explicit?

DavidArno commented 6 years ago

On the assumption that these sub-structs can all be implicitly cast from one to the other, I can see a real use for this: contextual method chaining.

For example, I have written some pattern matching classes that implement a lot of interfaces, which are explicitly implemented, each of which returns this via one of the interfaces. This allows me to only expose only those methods that are valid for a given context. For example, a cut down version of one of those files is:

internal sealed class Matcher<T1, TResult> : 
    IMatcher<T1>,
    IActionMatcher<T1>,
    IFuncMatcher<T1, TResult>,
    IActionWithHandler<IActionMatcher<T1>, T1>,
    IActionWhereHandler<IActionMatcher<T1>, T1>,
    IActionMatcherAfterElse,
    IFuncWithHandler<IFuncMatcher<T1, TResult>, T1, TResult>,
    IFuncWhereHandler<IFuncMatcher<T1, TResult>, T1, TResult>,
    IFuncMatcherAfterElse<TResult>
{
    ...
    IActionWithHandler<IActionMatcher<T1>, T1> IMatcher<T1>.With(T1 value)
    {
        _withValues = new List<T1> {value};
        return this;
    }

    IActionWithHandler<IActionMatcher<T1>, T1> IActionMatcher<T1>.With(T1 value)
    {
        _withValues = new List<T1> { value };
        return this;
    }

    IFuncWithHandler<IFuncMatcher<T1, TResult>, T1, TResult> IFuncMatcher<T1, TResult>.With(T1 value)
    {
        _withValues = new List<T1> { value };
        return this;
    }

    IActionWhereHandler<IActionMatcher<T1>, T1> IMatcher<T1>.Where(Func<T1, bool> expression)
    {
        _whereExpression = expression;
        return this;
    }

    ...
}

With this proposal though, I could use a set of sub-structs to achieve the same thing. This would bring a couple of advantages to my solution:

  1. The methods for a particular context would be in their own type, greatly enhancing readability,
  2. I could use a struct, rather than a class, which would bring significant performance gains

Of course in my case, we hopefully (according to @MadsTorgersen's presentation are the dotnet conference) should be seeing "proper" pattern matching in C# 7.3, so my code will become redundant at that point. But there are other situations where contextual method chaining is used that would also benefit from this.

fanoI commented 6 years ago

Another use of this could be in Actor based libraries as for example Akka where messages represent in reality record types and the ideal type for this should be struct (lightweight, no allocation, you can pass them by reference if you want using "ref", ...) but not having inheritance they soon become inadequate to be used and you have to resort to use classes again.

Another use are the so called Union Types one could like to set the Body of Messages to for example "Number" and expect that one could use it for int, long, double, decimal but you cannot as no one of these inherit from Number!

HaloFour commented 6 years ago

Even if this requires clr changes, im eager to propose that.

This certainly would require CLR changes. It's not C# that imposes this limitation, it's the CLR. And for a multitude of very good reasons. Any proposal like this will certainly have to start here and I would expect an uphill battle.

I have parent class with 4 childs. Class is light (only 8 bytes and a reference) and its immutable.

Not an uncommon pattern, for structs or classes. It sounds like you're not doing anything that hasn't been done a billion times before. I'd wager that there numerous existing solutions to your problem that don't involve rearchitecting a part of the runtime. Is this hierarchy closed? If so couldn't you add a tag byte field and have the members switch based on that field?

Note, the very concept of inheritance is going to increase the size of this struct. At the very least you'll need an additional field just for some kind of identity, otherwise the CLR will have no way of actually enforcing any type safety. Virtual methods would also require a v-table for virtual dispatch.

I really need this, please dont rush on this one and complain about violating rules, please dont kill creativity,

There is an exceptionally high bar for every proposal that is suggested here. It's been described as every proposal needs at least 100 points to be considered. Every proposal immediately starts with -100 points because it represents effort, time, resources and support for the lifetime of the language. The proposal needs to demonstrate how it makes up those 200 points. Furthermore, every proposal here needs at least one "champion" from the language design team (LDM), someone willing to bring it up and argue for it at the design meetings.

And that's just for the C# language. As this proposal requires a change to the CLR it also has to meet their bar for consideration which I'd expect is just as rigorous, if not moreso since every change will impact every user regardless of whether or not they actually use it.

maniero commented 6 years ago

Default method implementation in interfaces is not enough?

svick commented 6 years ago

@MkazemAkhgary

I really need this, please dont rush on this one and complain about violating rules, please dont kill creativity, but if you have strong reason why this should not be allowed, i appreciate to know.

That's not the way it works, it's the other way around: you need to have a really strong reason to make a change as significant as this.

I need this for performance reason. I have parent class with 4 childs. Class is light (only 8 bytes and a reference) and its immutable.

Could you expand more on what exactly are you doing? Maybe include a code sample? Also, what workarounds have you considered and why did you deem them inadequate? (If a problem has decent workarounds, it may not be worth it to modify the runtime and the language to fix it.)

MkazemAkhgary commented 6 years ago

@svick I actually found a workaround by defining implicit casts from top most types to base type. The classes were simple containers with an extra field like an indexer. something close to KeyValuePair.

Since I could have tons of those containers it would be not optimal to use class. also they are mainly used temporary. so I decided to change classes to struct and use it in a way that remains on stack (temporary).

this could be also achieved by tuple returns introduced in c#7. Since I'm not going to wait for years to see if this proposal gets chance to be worked on or not, I solved my problem the other way, but i guess ill leave it here for future thoughts.

MkazemAkhgary commented 6 years ago

As it turns out I don't have strong reason, but I'm pretty sure many small reasons will (can) form a good reason to start this significant change 😸 @svick

MkazemAkhgary commented 6 years ago

@HaloFour good to know. there is no rush on my side, ill leave it for community to solve this out.

tannergooding commented 6 years ago

I think, what would be helpful more than struct inheritance, is an easier way to create wrapper types.

That is, if you come across a type that is sealed (or maybe a type which isn't sealed but has a non-virtual method you want to override), you end up having to create a new type that wraps the original type and re-expose all of the methods that it exposed originally.

Ex:

sealed class SealedClass
{
    public void Method1()
    {
        // Do the Thing
    }

    public void Method2()
    {
        // Do the Thing
    }

    public void Method3()
    {
        // Do the Thing
    }
}

sealed class MySealedClass
{
    private SealedClass _value;

    public void Method1() => _value.Method1();

    public void Method2()
    {
        // Do Something Else
    }

    // Don't expose Method3()
}

So, I think, having some mechanism to easily say that MySealedClass wraps SealedClass, exposes Method1, modifies Method2, and hides Method3 would be beneficial.

yaakov-h commented 6 years ago

I don’t see how that’s actually related to this discussion. How do wrapper types solve the same problems as struct inheritance?

jnm2 commented 6 years ago

See @xoofx's awesome modification to CoreCLR and Roslyn where he does exactly this: http://xoofx.com/blog/2015/09/27/struct-inheritance-in-csharp-with-roslyn-and-coreclr/

tannergooding commented 6 years ago

@yaakov-h, you are wanting the ability to take non-inheritable type and extend or modify its behavior, correct?

To do that, you generally use inheritance. If you can't use inheritance (ex: C where inheritance doesn't exist, or sealed types in C#), you generally use composition.

At least in a simplistic view, inheritance just provides an easy way to say: "I start with the same functionality as this other type".

You can then provide new functionality or extend/modify/hide existing functionality (depending on some contract that the base type declared).

The same thing can be provided via composition, it just (today) requires writing a lot more explicit code.

If there was an easy way to create a wrapper type, then I don't think not being able to inherit would be as much of a problem.

yaakov-h commented 6 years ago

@tannergooding you're not so much extending or modifying it as creating a completely new type with different behaviour. Without polymorphism you can't treat it the same as an unmodified one.

tannergooding commented 6 years ago

There are some limitations, but for most use cases you can end up making it work (especially if you have the write interfaces declared).

iam3yal commented 6 years ago

@tannergooding What you're proposing is basically traits or proxy types and there's already some written proposals about it but the issue with a wrapper in this case as pointed above is that you create two types and in order to consume the wrapper as if it was the original type you have to do that through an interface and this will introduce boxing.

The following extends the type through composition but introduces boxing:

IFoo foo = new FooBar(new Foo());

The following extends the type and does not introduce boxing but consumers of the original type may not support the wrapper:

FooBar foo = new FooBar(new Foo());

So both of the approaches above aren't applicable and this is what this proposal try to address.

Thaina commented 6 years ago

I think what you want to do is actually solved by extension everything and extension with interface. Or shape in general

If we would have struct inheritance then I would like to have real inheritance for struct. It surely change size in child class and some inheritance related will not be the same as class inheritance. But it would be good to have

MkazemAkhgary commented 6 years ago

there are also cases when you have temporary builders, since some of those take advantage of polymorphism , if having builder struct was possible you could keep your builder on stack.

Joe4evr commented 6 years ago

if having builder struct was possible you could keep your builder on stack.

Why do people keep misunderstanding that as the primary purpose of structs? Read up.

The relevant feature of value types is that they have the semantics of being copied by value, not that sometimes their deallocation can be optimized by the runtime.

Thaina commented 6 years ago

@Joe4evr Even it not primary purpose we always utilize that feature because this feature is real deal

Joe4evr commented 6 years ago

There's nothing about the C# spec that prohibits one to make a custom C# runtime that has no "stack" and allocates everything on a memory heap, if they so wanted.

In the case of having temporary objects that you need to be polymorphic, you can use a pooling strategy.

tannergooding commented 6 years ago

There's nothing about the C# spec that prohibits one to make a custom C# runtime that has no "stack" and allocates everything on a memory heap, if they so wanted.

Sure this is allowed, the C/C++ specs technically allow this as well. In fact, in C#, the only way to "guarantee" call stack allocation is to use "stackalloc". C/C++ commonly have the alloca extension, but it is not officially part of their standards.

That being said, I highly doubt anyone would end up writing some an implementation for production use, and if it was, it would probably not gain widespread adoption. The perf and memory usage would likely be terrible and such an implementation would not be using some of the most basic functionality/optimizations available to the underlying hardware (which, to my knowledge, all major hardware today exposes the concept of a stack and instructions for working with it)

Additionally, such an implementation would need to create a new compiler (or fork and modify the existing one) that emits new IL (the CLR is entirely stack based), would need to create a new ABI (System V and the Microsoft ABIs require that value types are passed around on the call stack), would no longer interop with C/C++ or most other programs out there, etc.

So, from my view, regardless of whether or not the C# (or any language spec) technically allows value types to be heap allocated, provided they are still have value semantics (copied by value), this is just not something that anyone would reasonably do.

I don't think the spec can or will just make assumptions about a feature based on the fact that something is commonly (or will most certainly always be) true, but it can identify a problem based on those facts and add new features (such as ref readonly) that resolve them.

tannergooding commented 6 years ago

That being said, I don't think struct inheritance is one of those features, just from the perspective of required effort vs the perceived payoff.

It would, at the very least, require changes to both the C# and Runtime spec, as well as several implementation changes. It additionally has a chance of breaking assumptions in both the C# and Runtime implementations and therefore introducing subtle (or not so subtle bugs).

I think a runtime feature that allowed or guaranteed non-heap based allocation for reference types (which do allow inheritance) would end up being the more beneficial thing in the long run.

jnm2 commented 6 years ago

I think a runtime feature that allowed or guaranteed non-heap based allocation for reference types (which do allow inheritance) would end up being the more beneficial thing in the long run.

This does sound intriguing. I think the copy-by-value mental model could conflict with the inheritance mental model. Reference types on the stack would make so much more sense.

tannergooding commented 6 years ago

@jnm2, there are a few examples of such proposals here and there (https://github.com/dotnet/coreclr/issues/1784).

Thaina commented 6 years ago

Aside from that I also have other points. We now have return ref and ref local of struct. So it not really a must for struct to be copied or pass by value anymore. For me primary purpose of struct actually being a block of memory that won't need GC to do work

Even it was a register based machine that won't allocate struct in stack but on the heap, it would be implemented to deterministically deconstructed at the end of function

Another things is I think in the reality. struct being allocate on stack is actually the straightforward basic way to deal with value in function. The heap is the special one that we have for convenience in programming which emerge from pointer to struct. So it does not explicitly state in spec because it was the common. The class and heap is the one that need to be stated

But well, I am also not really support this feature much too. Now this conversation was stray into unrelated story about struct

DavidArno commented 6 years ago

@tannergooding & @jnm2,

I think a runtime feature that allowed or guaranteed non-heap based allocation for reference types (which do allow inheritance) would end up being the more beneficial thing in the long run.

For my use-case at least, I'm in complete agreement with this. "stack-based" ref types would be the ideal solution.

Thaina commented 6 years ago

I would prefer real struct inheritance (struct can inherit and add more field) than stackalloc class. Struct inheritance also have its own usage

But yeah, I agree that stackalloc object that could inherit, be it class or struct, will be useful

DavidArno commented 6 years ago

@Thaina,

I really don't care for the inheritance part, beyond interface implementation. It's non-heap-based, boxing-avoiding, instances that I want. Inheritance ranges from meh to merde in my view.

Thaina commented 6 years ago

@DavidArno That's remind me that I want IEnumerable from yield will be on stack, is it related?

MkazemAkhgary commented 6 years ago

I didnt though of it but as i read comments, if small classes can be held on stack it will be so beneficial. Though you never know when your class meets requirements for being kept on stack. Or at least, you cant be sure.

iam3yal commented 6 years ago

@MkazemAkhgary

Though you never know when your class meets requirements for being kept on stack. Or at least, you cant be sure.

This should never be a requirement though, it's an implementation detail, always was.

You can have a requirement for caching, allocations, object lifecycle, passing objects by value vs ref and anything else in your control but you shouldn't have a requirement that states that the object must live on either the stack or the heap, I mean, this would be a really weird requirement because it isn't something you control/own.

Note that I'm not speaking about the case of ref struct where this is clearly defined in the spec to be stack only.

Joe4evr commented 6 years ago

but you shouldn't have a requirement that states that the object must live on either the stack [...]

*looks at ref structs* 😗

jnm2 commented 6 years ago

And stackalloc. I do get the argument that caring about memory usage is good in some circumstances.

Joe4evr commented 6 years ago

stackalloc isn't part of the type definition the way that ref struct is/would be.

iam3yal commented 6 years ago

@Joe4evr Not sure how it's related because this is clearly something the developer can control and will be clearly defined in the spec and I clearly stated the following:

You can have a requirement for caching, allocations, object lifecycle, passing objects by value vs ref and anything else in your control

as opposed to regular structs or classes where it shouldn't matter whether they are allocated on the stack or heap as long as the behaviour of the code isn't changed which might be a subject to escape analysis in the future if and when it will be available to the CLR.

p.s. This might be poor wording on my part but by control I really meant things that are available in the language and the behaviour is clearly defined in the spec where you can reliably make assumptions and have guarantees about it.

Joe4evr commented 6 years ago

as opposed to regular structs or classes where it shouldn't matter whether they are allocated on the stack or heap

But ref structs are by their own definition not allowed to be allocated on the heap, so if you're consuming such an API, it's not in your control. Sure, it's all neatly specified, but it still is a requirement of the type definition.

iam3yal commented 6 years ago

@Joe4evr You're right but I wasn't referring to ref struct though I probably should edit my post and clarify that.

All I'm saying is that when a developer uses struct (not ref struct) or class then it wouldn't make sense for the developer to have the requirement that instances must be allocated on either the heap or the stack as this is an implementation detail and developers shouldn't make any assumptions about it.

alrz commented 6 years ago

I think struct inheritance is better represented as struct ADTs (https://github.com/dotnet/csharplang/issues/317#issuecomment-288863771), the compiler could turn it to a struct with an explicit layout. There will be some limitations regarding generics that need clr support though. Rust for instance had generic struct ADTs from day one, but it's compiling directly to native code and the type system is not depending on an external intermediate representation.

MkazemAkhgary commented 6 years ago

This breaks very common benefit of using structs over classes. if structs do support inheritance (even basic inheritance) it will cause them to occupy more space than their actual size.

I guess being able to keep small classes in stack provides just enough benefit without introducing any drawbacks, struct inheritance has its drawbacks so I'm closing this thread.

(I cant see any benefit from this proposal over this one https://github.com/dotnet/coreclr/issues/1784)

redknightlois commented 6 years ago

@MkazemAkhgary Just to add yet another use for why this is important feature (size restriction applied) that dotnet/coreclr#1784 cannot handle. We have issues like the ones this proposal would solve all over our codebase.

We had to devise static checks to avoid and external things like defining the SizeOf constant so we do not mess up.

Base struct type: https://github.com/ravendb/ravendb/blob/v4.0/src/Voron/PageHeader.cs Derived struct types: https://github.com/ravendb/ravendb/blob/v4.0/src/Voron/Data/RawData/RawDataSmallPageHeader.cs https://github.com/ravendb/ravendb/blob/v4.0/src/Voron/Data/RawData/RawDataSmallSectionPageHeader.cs https://github.com/ravendb/ravendb/blob/v4.0/src/Voron/Data/RawData/RawDataOverflowPageHeader.cs

Just to show a few examples.

This is typically an unmanaged code issue where you know your size, but your data may differ based on specific rules. We have also a few other tricks like using this to avoid the lambda context capture cost on very specific scenarios, but you get the idea.

HaloFour commented 6 years ago

@redknightlois

That's an interesting scenario but I can't imagine it would be one addressed by a proposal such as this. While your structs are the same size you are redefining their shape through these other types, and are relying on unsafe code to convert between one and another. I would assume that any kind of struct "inheritance" offered by the compiler and runtime wouldn't go that far.

Out of curiosity, what is the reason for having separate structs representing these different redefinitions? As you're already using explicit layout and field offsets which would enable you to construct a union of the different structs.

CyrusNajmabadi commented 6 years ago

Instead of struct inheritance, i'd much rather have the compiler allow for easier struct containment (a-la go). i.e. you could just compose a struct out of other structs, and have simple access to the contained structs members.

HaloFour commented 6 years ago

@CyrusNajmabadi

public struct Car {
    private int NumberOfWheels{ get; }

    public Car(int wheelCount) =>
        NumberOfWheels= wheelCount;
}

public struct Ferrari : Car {
    public Ferrari(int wheelCount) : Car(wheelCount) { }

    public void SayHiToSchumacher() =>
        Console.WriteLine("Hi Schumacher!");
}

public struct AstonMartin : Car {
    public AstonMartin(int wheelCount) : Car(wheelCount) { }

    public void SayHiToBond() =>
        Console.WriteLine("Hi Bond, James Bond!");
}

static class Program {
    static void Main() {
        var f = new Ferrari(4);
        Console.WriteLine($"A Ferrari has this many wheels: {f.NumberOfWheels}");
        f.SayHiToSchumacher();

        var a = new AstonMartin(4);
        Console.WriteLine($"An Aston Martin has this many wheels: {a.NumberOfWheels}");
        a.SayHiToBond();
    }
}
CyrusNajmabadi commented 6 years ago

@HaloFour Works for me. As long as you're not limited to 'single inheritance'. You should be able to compose as many structs as you want.

redknightlois commented 6 years ago

@HaloFour Because they are entirely different things, there are only a few fields that are shared along with the "union type". All the rest are different, so in a sense it is actually inheritance with an explicit discriminant key.

About why do you need them to be different structs instead of a union type? Because you can abuse the type system and how the JIT behaves to build traits and compile super-efficient code.