Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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 : struct "inheritance" #524

Closed
jmagaram opened this issue May 1, 2017 · 129 comments
Closed

Proposal : struct "inheritance" #524

jmagaram opened this issue May 1, 2017 · 129 comments

Comments

@jmagaram
Copy link

jmagaram commented May 1, 2017

Summary

Make it easier to create and use small domain-specific value objects through limited struct "inheritance"; this enables code-reuse and enables simplified struct names.

Motivation

We use classes for "big" objects like Person, Address, and Product but usually use generic string, int, Guid for the "small" values like EmailAddress, ProductTitle, and PersonId. But small intuitively-named domain objects have many benefits:

Validation in one place - Rather than sprinkling validation logic and throw expressions throughout the code to ensure that a string representing an email address is really a valid email address, you can put it in one place - the constructor of the class/struct representing the object. Then use that logic in the UI layer, business layer, and data access layer.

Type-safety - Even though a Product and Person might both be identified by a Guid it never makes sense to join on the two. Even though a Length and Weight are both represented by a float it doesn't make sense to compare them. It is an error to do LengthMeters + LengthInches without conversion. Small specialized domain objects make it more likely to fall into the pit of success because the compiler prevents meaningless comparisons/assignments between values in different domains.

Easier-to-understand code - We depend on parameter/tuple names to know what is going on rather than looking at the types of objects involved. This is a big problem with generic and anonymous delegates because parameter names are not allowed. If domain-specific types are used then parameter names become less necessary. For example, here is a view model that uses string, Guid, etc.

// Because a delegate like this is confusing, custom delegates must be defined
// Action<Guid, string, string, string, string> saveContactInformation;

delegate void SaveMeasurementsDelegate(Guid personId, float weightKg, float heightCm);

delegate void SaveContactInformationDelegate(Guid personId, string street, string city, string state, string email);

// When the user clicks OK the constructor delegates will be used to save info to a
// database. It is very difficult to see what actual parameters are necessary without
// navigating to the delegate definitions.
class EditPersonViewModelA
{
    public EditPersonViewModelA(
        SaveMeasurementsDelegate saveMeasurements,
        SaveContactInformationDelegate saveContactInformation)
    { }

    public (float weightKg, float heightCm) Measurements { get; set; }
}

This is the same view model but defined using custom primitives.

// Still need "height" parameter name on LengthCm
class EditPersonViewModelB
{
    public EditPersonViewModelB(
        Action<PersonId, WeightKg, LengthCm> saveMeasurements,
        Action<PersonId, Street, City, State, Email> saveContactInformation)
    { }

    public (WeightKg weight, LengthCm height) Measurements { get; set; }
}

A place to put the utility methods - Small domain objects need helper methods like Uri.CheckHostName, String.IsNullOrWhitespace, and Guid.Parse. If you have custom types then you've got a place to put methods like these.

You can use classes for these small domain objects, BUT…

  • Performance hit of using the heap versus stack
  • Must deal with null

Structs are the right technology for small values. BUT…

  • Without "inheritance" if you want to re-use certain code the ratio of boilerplate to interesting code gets very high
  • You can specialize structs using generics to get some code-sharing, but then the struct names become completely unwieldy to the point of being unusable. ValidatedString<string,Email> versus Email. Measurement<float, Length, MathFloat, ConvertLengthFloat> instead of just Length. Type-name aliases can help when defined within a single file but don’t work across a project or assemblies.

Example - EmailAddress (validated strings in general)

Here is some code that introduces an EmailAddress struct by wrapping a string. The EmailAddress struct has 43 lines of code but only 1 line is email-specific. By changing this single line you could repurpose the struct to validate all different types of strings - zip code, city, product name, etc. Without some kind of code sharing / inheritance it isn't easy to do that.

public interface IValidator<TValue, TError>
{
    /// <summary>
    /// Determines whether a value is valid and returns errors, if any. Custom error types makes
    /// it possible to display helpful messages in a UI layer.
    /// </summary>
    IEnumerable<TError> ValidationErrors(TValue value);

    /// <summary>
    /// Defines the value used when a struct is initialized with the default constructor. This
    /// does not have to be a valid value.
    /// </summary>
    TValue Default { get; }
}

public struct EmailAddressValidator : IValidator<string, string>
{
    static Regex _pattern = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$", RegexOptions.Compiled);

    public string Default => "somebody@domain.com";

    public IEnumerable<string> ValidationErrors(string value)
    {
        if (value == null) yield return "Can't be null";
        else if (string.IsNullOrWhiteSpace(value)) yield return "Can't be empty.";
        else if (!_pattern.IsMatch(value)) yield return "Doesn't look like an email address.";
    }
}

public struct EmailAddress : IEquatable<EmailAddress>, IEnumerable<char>, IComparable<EmailAddress>
{
    static EmailAddressValidator _validator = default(EmailAddressValidator); // The ONLY email-specific code
    string _value;

    public EmailAddress(string value) : this(value: value, validate: true) { }

    private EmailAddress(string value, bool validate)
    {
        if (validate && (!IsValid(value))) throw new ArgumentOutOfRangeException(nameof(value));
        _value = (value == NullValue) ? null : value;
    }

    public string Value => _value ?? NullValue;

    public override string ToString() => Value?.ToString();

    public override int GetHashCode() => Value?.GetHashCode() ?? -1;
    public bool Equals(EmailAddress other) => this.Value == other.Value;

    public static string NullValue = _validator.Default;

    public static implicit operator string(EmailAddress e) => e.Value;

    public static explicit operator EmailAddress(string s) => new EmailAddress(s);

    public static IEnumerable<string> ValidationErrors(string value) => _validator.ValidationErrors(value);

    public static bool IsValid(string value) => !ValidationErrors(value).Any();

    public static (EmailAddress result, IEnumerable<string> errors, bool isValid) TryParse(string input)
    {
        var errors = ValidationErrors(input);
        var isValid = !errors.Any();
        var result = isValid ? new EmailAddress(value: input, validate: false) : new EmailAddress();
        return (result, errors, isValid);
    }

    public IEnumerator<char> GetEnumerator() => ((IEnumerable<char>)Value).GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<char>)Value).GetEnumerator();

    public int CompareTo(EmailAddress other) => Value.CompareTo(other.Value);
}

/// <summary>
/// Here is part of a generic struct that could be used to validate all different kinds of
/// strings. Because inheritance is not supported, the validator is injected via a type argument
/// (seems like a hack). There are some problems: (1) Everywhere the type is used it has an
/// unwieldy name rather than something intuitive like 'EmailAddress'. (2) It is not possible to
/// attach type-specific instance methods, other than extension methods. (3) It is not possible
/// to attach type-specific static methods.
/// </summary>
public struct ValidatedString<TValidator, TError> where TValidator : struct, IValidator<string, TError>
{
    static TValidator _validator = default(TValidator);
    // Rest of the code not shown
}

Example : Units-of-measure

I quickly threw together some code to do units-of-measure-aware math. This is an example of how quickly the number of type parameters can get out of control. If you had an API that expected a float-based Length it would be inappropriate and cumbersome to define a method parameter type as Measurement<float, Length, MathFloat, ConvertLengthFloat>.

[TestMethod]
public void CanAddUnitsRequiringConversion()
{
    var x = new Measurement<float, Length, MathFloat, ConvertLengthFloat>(1f, Length.Feet);
    var y = new Measurement<float, Length, MathFloat, ConvertLengthFloat>(2f, Length.Inches);
    var total = (x + y).ConvertTo(Length.Inches);
    Assert.AreEqual(14f, total.Value);
    Assert.AreEqual(Length.Inches, total.Units);
}

Detailed design

I don't know much about IL, Roslyn, CLR, and the technical differences between structs and classes so bear with me. The overall goal is to make it easier to build and use small domain-specific value objects. This can be achieved by (1) sharing code between structs to limit boilerplate, and (2) enabling user-friendly struct names. One way to do this is to allow structs to "inherit" from other structs, like this.

public abstract struct Validated<T> where T : struct
{
    T? _value;

    public Validated(T value)
    {
        if (!IsValid(value)) throw new ArgumentOutOfRangeException();
        _value = (value.Equals(DefaultValue)) ? new T?() : value;
    }

    public T Value => _value ?? DefaultValue;
    protected abstract bool IsValid(T value);
    protected abstract T DefaultValue { get; }

     // Boilerplate for Equals, GetHashCode, conversions, ToString, etc. omitted
}

public struct SurveyRating : Validated<int>
{
    public SurveyRating(int value) : base(value) { }
    public const int MinimumInclusive = 1;
    public const int MaximumInclusive = 9;
    protected override int DefaultValue => MinimumInclusive;
    protected override bool IsValid(int value) => value >= MinimumInclusive && value <= MaximumInclusive;
}

The syntax is nearly the same as for class inheritance so there isn't too much to say. I suspect that prohibiting virtual methods would make it easier to implement. Abstract methods would enable code-reuse and specialization, especially since it isn't possible to customize behavior with constructor parameters - no default constructor. Because there are no virtual methods it should be possible to know exactly what member is being called at compile-time. Under-the-covers, using reflection, the derived struct may not necessarily look like it inherits from anything. SurveyRating would compile to something nearly identical to what you'd get if you copy-pasted the base-struct code into it and prefixed every member with its fully-qualified path name. Maybe there would need to be other limitations; just like today a generic constraint like where T : struct, SurveyRating or where T : struct, Validated<U> would not compile; constraints must be classes or interfaces.

Drawbacks

When using code that expects the built-in string, int, DateTime primitives, like Entity Framework, various LINQ providers, and serialization libraries, you'd have to do conversions to/from your specialized domain to the general-purpose types they expect.

Lots of code is needed to wrap primitive types to avoid losing useful functionality of those types. For example, when wrapping numeric types you need to write a lot of code to regain the math operators.

Alternatives and partial solutions

Aliases on structs and string

Type name aliases mentioned in #410 like public alias Length = Measurement<float, Length, MathFloat, ConvertLengthFloat> solves the problem of unwieldy names, and lets you use those friendly names across file/project/assembly boundaries. But it has some drawbacks: (1) Without abstract methods, you can only reuse struct code with generics - relying on a default(T) to access its static methods. This was a new technique to me and at first it seemed a bit like a hack, but I'm getting more comfortable with it. It causes type names to keep expanding (more and more type parameters) It seems less OOP, less intuitive, and more heavy-weight than overriding an abstract method like bool IsValid(string s). (2) Type-name aliases don't allow you to introduce type-specific instance or static methods, or use type-specific naming.

If ALL you had was a type name alias like public alias EmailAddress = string that crossed file/assembly boundaries and provided NO restrictions on how to create one other than an explicit cast then that would still be pretty useful. Users of the code might go hunting for the official "CreateAndValidateEmailAddress" method rather than just doing an explicit cast on a string. Not totally type-safe but pretty good.

If we had a type aliasing feature I'd want IMPLICIT conversion from a complex named thing to its alias. A method that accepts a LengthFloat should implicitly accept a Measurement<float, Length, MathFloat, ConvertLengthFloat>. But a method that accepts an EmailAddress should only allow a string that has been EXPLICITLY converted. This kind of syntax could work:

public alias Length = implicit Measurement<float, Length, MathFloat, ConvertLengthFloat>
public alias EmailAddress = explicit string

The alias feature could be beefed up to include some built-in validation:

public alias StarRating = int where value>=1 && value<=5 with default 5;
public alias EmailAddress = string where value matches regex…

This alias feature could be beefed up even more to provide a natural place for type-specific instance and static methods.

public alias Email restricts string {
    public explicit Email(string input) { // casting constructor
    {
        if (!IsValid(input)) throw new ArgumentOutOfRangeException();
        value = input; 
    }

    public Email Normalize() => new Email(value.Trim().ToLower());

    public static Email(string user, string domain) => new Email($"{user}.{domain}");

    public static bool IsValid(string input)
    {
        Regex r = new Regex("pattern");
        return r.IsMatch(input);
    }
}

Code generation

T4 code templates. I've tried this. It works eventually but is cumbersome due to lack of VS IDE support. You've got to learn a new language (the template language). Maybe some other form of code-generation is in the plan that could work.

Classes instead of structs

Use classes instead of structs. Must be careful dealing with null. Maybe for a lot of apps the performance hit of heap vs. stack doesn't matter. Our computers are insanely fast so I'd be surprised if it would negatively affect typical desktop apps. This is probably more important for things like web services.

Related issues

Internal aliases #259
Type aliases abbreviations #410
Constrained types #413
Better support for delegates, including naming parameters #470

@HaloFour
Copy link
Contributor

HaloFour commented May 1, 2017

Sounds like most of the use cases here would be covered by traits/"default interface methods". You couldn't store the state of the field in this abstract struct, but you could potentially expose a protected accessor property to retrieve it's value.

To actually implement what you've described above would seem to involve a lot of CLR changes. Is Validated<T> actually a type? Can a method accept a parameter of that type? If so, how can that method invoke any of these abstract members if not by virtual calls? If Validated<T> isn't its own type but some kind of template how do you reference them from other assemblies?

The SurveyRating use case is pretty dubious because as a struct you cannot avoid the zero-init default value which is instantly invalid. You might not have to worry about null, but you always have to worry about zero.

@whoisj
Copy link

whoisj commented May 1, 2017

Personally, I'd rather see the "Default Interface" solution substitute any kind of inheritance. Especially for structs.

@CyrusNajmabadi
Copy link
Member

Tagging @gafter @MadsTorgersen . This relates to recent discussions we've had about structs and how Default-Interface-Members play along with them.

I think the desire to be able to have traits for structs will be very real. I'm hoping that if we take an approach that doesn't support struct-traits initially, we won't end up boxing (har har) ourselves into a corner such that we can't add it in the future.

@jmagaram
Copy link
Author

jmagaram commented May 2, 2017

It seems like a the default-interface-implementation feature resembles an abstract class, with some functionality built-in but NO instance fields. So maybe you'd have something like this interface. The boilerplate around GetHashCode, ToString, and some other stuff would be handled and would not need to be re-implemented each time.

public interface IValidatedString : IEnumerable<char>
{
    public abstract string Value { get; }
    public override int GetHashCode() => Value?.GetHashCode() ?? -1;
    public override string ToString() => Value?.ToString();
    public static implicit operator string(IValidatedString s) => s.Value; // Allowed?
    public IEnumerator<char> GetEnumerator() => Value.GetEnumerator();
}

But many of the features in my EmailAddress struct require creating a new instance of that specific struct, or comparing one EmailAddress to another. I don't want to compare any IValidatedString to another IValidatedString since they might be in different domains (Email vs. CountryCode). Would a "recursive" constraint like this work?

public interface IValidatedString<T> : IEquatable<T> where T : IValidatedString<T>, new()
{
    public static abstract IEnumerable<string> ValidationErrors(string value);

    public static bool IsValid(string input) => !ValidationErrors(input).Any();

    public abstract string Value { get; }

    private abstract void SetValueWithoutValidating(string input);

    // Probably can't implement the real constructor here
    public static abstract T New(string input)
    {
        if (!IsValid(input))
            throw new ArgumentOutOfRangeException();
        T result = new T();
        result.SetValueWithoutValidating(input); // Must be mutable; ugh!
        return result;
    }

    // Try to create via explicit cast from string
    public static explicit operator T(string input) => New(input);

    // Useful utility method to try to construct an object by parsing a string
    public static (T result, IEnumerable<string> errors, bool isValid) TryParse(string input)
    {
        var errors = ValidationErrors(input);
        var isValid = !errors.Any();
        T result = new T();
        if (isValid)
        {
            result.SetValueWithoutValidating(input);
            return (result, errors, isValid);
        }
    }

    // Compare ONLY to others of the same domain      
    public bool Equals(T other) => this.Value == other.Value;
    public override bool Equals(object obj) => (obj is T other) && this.Equals(other);
}

public struct EmailAddress : IValidatedString<EmailAddress>
{
    private string _value;

    public EmailAddress(string input)
    {
        if (!IsValid(input)) throw new ArgumentOutOfRangeException();
        _value = input;
    }

    public static abstract IEnumerable<string> ValidationErrors(string value)
    {
        // Custom validation logic here
    }

    public abstract string Value { get; } => _value;

    private abstract void SetValueWithoutValidating(string input) => _value = input;
}

Regarding the "zero" case for SurveyRating, I'd work around that like I did in the EmailAddress case. public int Value => _isInitialized ? _value : DefaultValue; I think this is common pattern for how to deal with the inability to author default constructors for structs.

As for whether Validated is actually a "type" I don't know. Assuming...

abstract struct Food { }
struct Fruit : Food { }
struct Banana : Fruit { }

Food food; // Won't compile
Fruit fruit = new Fruit(); // Ok
fruit = new Banana(); // Won't compile
List<Fruit> fruits = new List<Fruit>(); // Ok
fruits.Add(new Banana()); // Won't compile

Maybe I'm describing something more like a "template". If you couldn't subclass Food from another assembly then the feature would lose a lot of value. Like I said before, this "inheritance" approach is really just shorthand for sharing code between one struct and another.

There are many times I attempt to move in the "functional" coding direction by using immutability and value semantics for non-null and equality. I also like the DDD approach of having objects in my code that model the real-world problem domain. For smaller objects this leads me toward structs. Unfortunately structs lack the flexibility of classes - no default constructor and no inheritance - so they end up being a pain to use.

@DavidArno
Copy link

Structs were never designed to be used with inheritance, even to the extent that it's generally a good idea to avoid having structs implement interfaces as any interaction with that interfaces necessitates boxing the value.

As it mentions in the C# Language Design Notes for Apr 19, 2017, it would be incredibly hard to use trait interfaces with structs in any meaningful way without breaking changes or performance issues.

If you want a type that supports inheritance, use a class. If you want multiple inheritance, use trait-interfaces with those classes when that feature becomes available. If you want a struct, use composition, which is already available. Nothing else is needed.

@whoisj
Copy link

whoisj commented May 2, 2017

For those of us who prefer to avoid the HEAP when possible, Default Interfaces are the first step towards that kind of thing. We'll also need a borrow operator, struct destructors, and ideally empty struct constructors.

@DavidArno
Copy link

For those of us who prefer to avoid the HEAP when possible, Default Interfaces are the first step towards that kind of thing...

Really? Surely using structs and ref local/returns should be that first step? Or maybe the first (and last) step should be to just stick with C and manage all your own memory... 🤷‍♂️

@jmagaram
Copy link
Author

jmagaram commented May 2, 2017

@DavidArno , do you agree with the goal of being able to easily create small value-based objects like EmailAddress, PersonId, ProductName and numeric structs with clearly-defined units? If yes, what solution do you recommend? Classes aren't value-types and tax the heap. The best struct composition method I know about is to use generics but then you end up with unwieldy type names like Measurement<float,Length,FloatMath,LengthConversions>. Generic improvements like #395 will help get rid of some of the type arguments but you'll never end up with simple names like EmailAddress or Length. T4 works but is a hassle.

@DavidArno
Copy link

DavidArno commented May 2, 2017

Classes aren't value-types and tax the heap

It seems to have become trendy to bitch about the heap recently. Yet managed memory using a heap and garbage collection has served millions of developers well for years. Sure, if you are writing a game for a mobile device, you may need to sacrifice every last gram of code simplicity to maximise performance. But for the vast majority of folk, the worst thing about classes has always been, and will always be null and heap/garbage collection performance is not something they never have to worry about.

do you agree with the goal of being able to easily create small value-based objects like EmailAddress, PersonId, ProductName and numeric structs with clearly-defined units? If yes, what solution do you recommend?

Type abbreviations get my vote.

@CyrusNajmabadi
Copy link
Member

It seems to have become trendy to bitch about the heap recently.

Because it's a widespread problem that we hear about across the board. This includes internal customers and large amounts of external customers (including non-game developers). This feedback has been nonstop over the years and has only grown larger as time has gone on.

There are very real reasons that things like Nullable and tuples are value types, and it's precisely because we want these features to be wildly usable, and being expensive types works directly against those goals for so many customers.

@DavidArno
Copy link

DavidArno commented May 2, 2017

Because it's a widespread problem that we hear about across the board. This includes internal customers and large amounts of external customers (including non-game developers). This feedback has been nonstop over the years and has only grown larger as time has gone on.

And just how much of that is genuine problems of using the heap and how much misinformation and urban myth, such as a common one that StringBuilder should be used as it avoids triggering garbage collection? Whilst clearly not universally true, in my experience GC issues are normally an indication of a bad design and a poor understanding of memory management that is better solved using memory profiling tools and some restructuring, rather than moving to ref/nullable structs.

Endlessly giving these folk more and more esoteric struct features just treats the symptoms and doesn't address underlying bad designs.

@jmagaram
Copy link
Author

jmagaram commented May 2, 2017

Uncle! I'll stick with classes until proven performance problems (or null) gets in the way.

@CyrusNajmabadi
Copy link
Member

And just how much of that is genuine problems

They're very genuine problems. We work closely with partners and we've seen what's going on with them. We also see this issue directly in all our own first party efforts which have business critical scalability needs.

Consider the core code that goes into ASP.net. This is not game code. But it absolutely needs to be efficient as every piece of ASP business logic out there runs on top of it.

Remember that C# is the primary language that is used to build the platforms and libraries that so many domains sit on top of. You're right that many devs are served by the heap. But what that fails to recognize is that so much critical work is not served by the heap, and those devs you mention depend on those critical pieces :)

@Thaina
Copy link

Thaina commented May 3, 2017

default interface or anything related could not cover field inheritance. Block of memory with direct access is somehow convenient

Instead of the need to write some more code just to do the inheritance imitation. It far more easy to just inherit the struct

And also it make us could manage construction,readonly,namecollision and so on

@DavidArno
Copy link

Consider the core code that goes into ASP.net. This is not game code. But it absolutely needs to be efficient as every piece of ASP business logic out there runs on top of it.

Remember that C# is the primary language that is used to build the platforms and libraries that so many domains sit on top of. You're right that many devs are served by the heap. But what that fails to recognize is that so much critical work is not served by the heap, and those devs you mention depend on those critical pieces :)

So if the heap doesn't work well for frameworks like ASP.Net, then obviously those folk will now be making heavy use of structs and ref returns, right? Yet a search of that code base shows zero ref returns. That suggests that writing highly efficient code using the heap is working just great for them.

But there again why not just add full inheritance to structs, eh? It's what the folk want, so it must be good, no? 🤷‍♂️

@CyrusNajmabadi
Copy link
Member

then obviously those folk will now be making heavy use of structs

That's not the claim that was made.

But there again why not just add full inheritance to structs, eh?

That's not what's being asked for.

--

It's hard to engage with you David as you seem to resort to strawmen a lot.

@Thaina
Copy link

Thaina commented May 3, 2017

@DavidArno Currently now I can't compile my code using ref return yet (tuple too, both got compile error when I deploy it to AWS Lambda Server)

Sometimes new feature just not become standard immediately because it cannot work with old code so people need to use old patter for such times. It need sometimes such a years to have people acknowledge and adopt any new feature. This was always a problem since generic in C# 2 and async in C# 5. Most people need to have sometimes to get used to new feature

Not to mention many people write code to have backward compatibility. And not to mention most code is legacy and they don't want to change old one. And all things rely on that legacy library would be more likely to adopt the same pattern as the library their used (When people use library that still use Begin/End pattern they also not try to use async/await with it)

Even today there are people still using HashTable instead of generic Dictionary<,>

C# in unity still cannot even use await and it very frustrating but yield WaitFor is used everywhere because it is legacy that unity cannot changed yet

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented May 3, 2017

David, the claim is not that in order to get fast code that you need something like "structs everywhere". The issue is more that eventually you get to the point where you you have squeezed what you can out of the system, and you still face things like scalability bottlenecks preventing you from fully utilizing the system, and you are spending cycles on things that aren't desirable.

Asp, for example, has come to us many times with issues they've run into where they've done everything possible on the existing .net fx + C# and they are still being hit with perf issues that there is no existing solution for.

This is one of the drivers around the Span<T> and "ref-like" work that we're doing. Will Asp end up using this everywhere? Highly unlikely. But they intend to use these capabilities judiciously in teh core areas where these language/runtime features can end up having a big impact on scalability.

Again, contrary to your strawman, there is no need for them to make "heavy" use of this feature. They simply need to make effective use of the feature for their domain.

The same is true for something like "ref returns". In realistic codebases, you might only end up using ref-returns in a smattering of places. For example, some of the core update loops of your game engine. The intent is not that you would ref-return your entire codebase (indeed, that could likely lead to worse performance). Instead, the intent is to give you this capbility so that in the few places you need it, you actually have it available as it can amount of a significant win in performance.

--

I also wanted to address this point on a personal level:

It seems to have become trendy to bitch about the heap recently. Yet managed memory using a heap and garbage collection has served millions of developers well for years.

Oh how i wish taht were so. Roslyn itself has direct experience on how that isn't the case, and the problems with the heap, and GC have been a constant source of issues with us from the start. This is why, for example, types like SyntaxList and SyntaxToken are structs. It's not because we liked them better as structs, it's because the performance problems we had with these being classes were significant.

Similarly, we invested ImmutableArray (another struct) precisely because we had the need for a collection with immutable semantics that was 0-overhead over the underlying array data.

Likewise, many core parts of Roslyn cannot use Linq, Lambdas, or Iterators, because the cost of those features is simply too high and there is no non-heap alternative to them available. Sure, 90% of roslyn can still use the heap just fine. But it is absolutely the case that neither the heap, nor GCs is sufficient for us to get the performance we need.

This is why we've ensured that Tuples can be used effectively even in perf critical code. It's why we've invested in ValueTuple. It's why we're looking deeply into more and more features that provide rich expressiveness, while not hitting the heap. Our own direct experience makes us painfully aware of exactly the sorts of issues that so many customers (1st and 2nd party) are facing all the time.

@DavidArno
Copy link

DavidArno commented May 3, 2017

Again, contrary to your strawman, there is no need for them to make "heavy" use of this feature. They simply need to make effective use of the feature for their domain.

Wow, that's low, even for you. Not content to turn my "makes zero use of..." into a "doesn't make heavy use of..." strawman, you then attribute that strawman creation to me!

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented May 3, 2017

You stated: "then obviously those folk will now be making heavy use of structs"

I was literally referring to your exact statement.

--

Again, to make the point clear: Heap issues are felt consistently by teams working to reach high levels of performance and scalability. We've been hearing this for 10+ years and we've been working closely with many teams, both internal and external about this over the years. Asp.net is one such team, and their direct experiences have been driving several efforts recently to address both that:

  1. the heap, allocations, and GC are a top concern for them.
  2. the existing language/platform features (including structs as they exist today) are still not proving to have enough perf issues of their own to warrant new language/platform work.

None of my claims have been that people with perf needs would be heavily using these features. that's your own claim, which you then proceeded to knock down.

@CyrusNajmabadi
Copy link
Member

And again, please read about our own direct experiences with the heap+allocations+GC. These issues are significant and man decades of effort have been spent on the problem so far in roslyn alone.

Heck, i've been putting in tons of effort into this space in several roslyn features, to attempt to get things to scale up even more than they can today, precisely because we have people using our products that are having memory issues, and we want to find ways to alleviate things for them.

@DavidArno
Copy link

DavidArno commented May 3, 2017

You stated: "then obviously those folk will now be making heavy use of structs"

I was literally referring to your exact statement.

Oh, so you were. That's embarrassing. My apologies then as I was wrong to accuse you in that way.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented May 3, 2017

@DavidArno Just a personal piece of advice. We have a lot of customers that we have to consider when thinking about the language and where it is going. Some of these customers themselves are quite important and represent huge platforms themselves with many millions of their own customers.

Please understand that your own experiences and your own views on what is or isn't important may not be at all representative of the rest of the ecosystem. It's also true that your views may be representative of some group (including a large one), but that we may still feel work is important if the net impact is significant. i.e. we may do work that will help out a few dozen direct customers, but which then ends up impacting millions of customers indirectly.

I would recommend not just jumping to the conclusion that something is "trendy" and doesn't actually represent a real and important concern out there. As you have seen (and lamented), we actually tend to be quite conservative over here. So when we actually feel like something is worth addressing, it's usually because we now have so much data, and so much continued feedback about the necessity of this work, that we've gone far past 'trendiness' and we're doing things because they're actually important. :)

@jmagaram
Copy link
Author

jmagaram commented May 4, 2017

Other people recommending something very similar: dotnet/roslyn#104

Pseudo-inheritance https://roslyn.codeplex.com/discussions/562037

Related to method contracts dotnet/roslyn#119; however if you're writing a contract to ensure that a parameter is in a particular range that could be solved more elegantly by defining a new type whose name clearly identifies what is expected.

Databases have column-level constraints for length, nullability, and range expressions.

Another proposal (with same EmailAddress case), like aliases dotnet/roslyn#58

Single-case union types in F#, type CustomerId = CustomerId of int
From https://fsharpforfunandprofit.com/posts/discriminated-unions/
"This approach is feasible in C# and Java as well, but is rarely used because of the overhead of creating and managing the special classes for each type. In F# this approach is lightweight and therefore quite common."

As I scan the list of ideas for the next version of C#, it seems like there are many big ideas with a big payoff. I am a hobbyist programmer so I can't argue from real-world experience that this struct inheritance capability is relatively important. As I write code, all those generic Guid, string, int references don't map to the way I think about the problem and I worry about invalid data getting passed around. Enums feel good/specific in a way that strings and ints don't. But fixing it via custom structs (or even classes) for every CustomerId, ProductId, Email, etc. is cumbersome and non-idiomatic. I would love to see some kind of lightweight solution here that lets me use friendly domain-specific type names throughout my code, across assemblies, and makes it a bit harder (like an explicit conversion) to pass around invalid data.

@mattnischan
Copy link

Struct inheritance would actually have been very useful for a better ValueTuple implementation that doesn't rely on attributes for item names. For example:

public class A
{
    public (int Count, int Total) Sum(IEnumerable<int> items)
    {
        var count = 0;
        var total = 0;
        foreach(var num in items)
        {
            total += num;
            count++;
        }
        
        return (Count, Total);
    }
}

public class B
{
    public int GetSum(IEnumerable<int> items)
    {
        var a = new A();
        return a.Sum(items).Total;
    }
}

Could have compiled to something like:

public struct ValueTuple_A_Sum_int_int : ValueTuple<int, int>
{
    public count
    {
        get { return Item1; }
        set { Item1 = value }
    }

    public total
    {
        get { return Item2; }
        set { Item2 = value; }
    }
}

public class A
{
    public ValueTuple_A_Sum_int_int Sum(IEnumerable<int> items)
    {
        var count = 0;
        var total = 0;
        foreach(var num in items)
        {
            total += num;
            count++;
        }
        
        return new ValueTuple_A_Sum_int_int(total, count);
    }
}

public class B
{
    public int GetSum(IEnumerable<int> items)
    {
        var a = new A();
        return a.Sum(items).Total;
    }
}

@DavidArno
Copy link

@CyrusNajmabadi,

@DavidArno Just a personal piece of advice. We have a lot of customers that we have to consider when thinking about the language and where it is going. Some of these customers themselves are quite important and represent huge platforms themselves with many millions of their own customers.

This sort of thing makes me wonder if open-sourcing C# was such a good idea after all. As we saw with scope leakage for out vars, no matter how large the community around that open source project grows, it will still remain tiny compared with those important customers. No matter how united its opposition to something, it will be ignored if those big customers want that something.

@lachbaer
Copy link
Contributor

lachbaer commented May 5, 2017

@DavidArno He who pays the piper, calls the tune. After all we can make our points and maybe give some unexpected input to the team. They in reverse forward that input to their key customers. IMHO, it is better to have a little say than none at all. 😊

@HaloFour
Copy link
Contributor

HaloFour commented May 5, 2017

@DavidArno

I'm also going to assume that the number of active participants in these discussions on github are vastly outnumbered by the people with whom Microsoft communicates regularly about product direction and development. It's likely less that our voice matters less, there just aren't as many of them. It would be nice to drive all of that discussion through one place, like here, but I'm sure that's not possible.

@DavidArno
Copy link

@HaloFour,

You are right, of course. I had hoped that them creating this repo would make it that one place, but that probably won't happen.

@HaloFour
Copy link
Contributor

HaloFour commented May 5, 2017

@DavidArno

I also think that they expected a lot more community interaction. I've seen in mentioned on threads before that there was disappointment in how few active participants were drawn to either the Codeplex or Github repos. There's probably, what, 2-3 dozen of us that regularly comment/complain/debate here? I've tried a number of times to get coworkers and friends involved at some level and most just claim that they don't have the time.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Mar 7, 2022

I think that proposal has already been implemented inside VS2022 with C#10

That proposal has not been implemented. It is discussing a different concept. The implemented idea is that you can statically import a type in a file and get the extensions defined within that static-class brought into scope.

THe referenced proposal is stating that if you have an instance of any named type, that you also bring in automatically any extensions to it defined in teh same namespace that the named type was declared in. in effect, it's the opposite of what we support. you would need no usings at all but could still get extensions.

@CyrusNajmabadi
Copy link
Member

This still isn't as good as struct extension inheritance

You'll have to provide explanations in order to get others to understand why. :)

@najak3d
Copy link

najak3d commented Mar 7, 2022

That proposal has not been implemented. It is discussing a different concept. The implemented idea is that you can statically import a type in a file and get the extensions defined within that static-class brought into scope.

THe referenced proposal is stating that if you have an instance of any named type, that you also bring in automatically any extensions to it defined in teh same namespace that the named type was declared in. in effect, it's the opposite of what we support. you would need no usings at all but could still get extensions.

I'd say that putting my @CyrusNajmabadi hat on -- the new C# feature of global using's provides the near equivalent benefit... you just state it once, and now you have what you want automatically - everywhere inside your project.

Isn't that what you say about nearly everything I propose? Yet here is a proposal where it's very very true -- yet you don't come to this same conclusion? (this is why I tend to think your comments on my ideas may be biased against me specifically)

@najak3d
Copy link

najak3d commented Mar 7, 2022

This still isn't as good as struct extension inheritance

You'll have to provide explanations in order to get others to understand why. :)

I think most know why. It's more direct/clear, not accomplished via a more mysterious "global using classExtension" statement found somewhere within your project. Plus, you get fine grained control everywhere, easily, to determine if you want to see those new extensions or not. The struct extension notation is superior without doubt, and I've already explained it (although it should be obvious). However, I can accept the Extensions/Roles solution, as it still gets the job done easy enough.

@CyrusNajmabadi
Copy link
Member

I'd say that putting my @CyrusNajmabadi hat on -- the new C# feature of global using's provides the near equivalent benefit...

It is not the same benefit. One requires you to even know tehre are extensions and then add teh namespace so you can use it. The other no knowledge or explicit step to make them available :) They're solving different problems.

@CyrusNajmabadi
Copy link
Member

Yet here is a proposal where it's very very true -- yet you don't come to this same conclusion?

I said nothing about conclusions. I was simply linking you to the corresponding issue. Why do you think my conclusions are different?

@najak3d
Copy link

najak3d commented Mar 7, 2022

I'd say that putting my @CyrusNajmabadi hat on -- the new C# feature of global using's provides the near equivalent benefit...

It is not the same benefit. One requires you to even know tehre are extensions and then add teh namespace so you can use it. The other no knowledge or explicit step to make them available :) They're solving different problems.

Correct - there is nominal benefit here. You omit the requirement of the library user to "know about the extensions". I realize this as this is part of why I Extensions/Roles isn't the optimum notation for solving my desire for "struct extension derivations". I don't want users to have to "know about these mysterious/helpful extensions" (which are easily pulled in, once you know about them).

@CyrusNajmabadi
Copy link
Member

I don't want users to have to "know about these mysterious/helpful extensions" (which are easily pulled in, once you know about them).

Right. That's why i pointed you to this proposal as it solves the issue you seem to be concerned with, and it doesn't require creating a duplicate system for defining extensions.

(this is why I tend to think your comments on my ideas may be biased against me specifically)

I literally was linking you to this to support the objective you were claiming you wanted. Why would i do that if i were biased against you?

I was showing that there's a proposal, and community interest in solvign such a topic. And that proposal shows a way to solve things taht doesn't require a duplicative approach to adding members that is outside of the extensions-space of the language..

@najak3d
Copy link

najak3d commented Mar 7, 2022

Yet here is a proposal where it's very very true -- yet you don't come to this same conclusion?

I said nothing about conclusions. I was simply linking you to the corresponding issue. Why do you think my conclusions are different?

Because you recommended I look at that proposal without noting that "It's mostly been satisfied now by C#10 with global usings", and therefore, may no longer be viable.

@CyrusNajmabadi
Copy link
Member

Because you recommended I look at that proposal without noting that "It's mostly been satisfied now by C#10 with global usings", and therefore, may no longer be viable.

I literally just explained that that isn't the case. In a post you've already responded to. I do not think that proposal has been satisfied.

@najak3d
Copy link

najak3d commented Mar 7, 2022

I don't want users to have to "know about these mysterious/helpful extensions" (which are easily pulled in, once you know about them).

Right. That's why i pointed you to this proposal as it solves the issue you seem to be concerned with, and it doesn't require creating a duplicate system for defining extensions.

(this is why I tend to think your comments on my ideas may be biased against me specifically)

I literally was linking you to this to support the objective you were claiming you wanted. Why would i do that if i were biased against you?

I was showing that there's a proposal, and community interest in solvign such a topic. And that proposal shows a way to solve things taht doesn't require a duplicative approach to adding members that is outside of the extensions-space of the language..

OK - I'll accept that. I think that proposal is nice too. If an extension exists inside the same namespace (even if that namespace is not from the base library) -- I'd like to see it "auto-included" without a "using".

I see your point. I do like this proposal. I retract my statement about you being biased against me. :)

@CyrusNajmabadi
Copy link
Member

Correct - there is nominal benefit here. You omit the requirement of the library user to "know about the extensions". I realize this as this is part of why I Extensions/Roles isn't the optimum notation

Again... that's the point i'm making. You are assuming extensions/roles work a certain way and thus are not sufficient for your needs, and thus you've proposed an entirely disparate system for solving the same problems. I'm pointing out that extensions/roles can not only solve the problems they aim to solve, but yours as well, through proposals liek the one linked. We dont' have a need to build some entirely new struct-inheritance system to solve that particular problem when there are much simpler and effective solutions that would solve thigns for roles/extensions, as well as all the extensions shipped so far.

@najak3d
Copy link

najak3d commented Mar 7, 2022

When I kept griping that extensions are "namespace-wide", this was based on a misunderstanding that you and I BOTH had. Neither of us seemed aware that you could easily just pull in extensions from a single class. Had you known this, you could have corrected me on it near the start of my griping. This would have removed a good portion of my ammunition against Extensions in general.

So looks like we both learned something today, thanks to @HaloFour .

@CyrusNajmabadi
Copy link
Member

Here's a meta point that i think is worth getting across. Say an existing feature solves 98% of a problem space. We're not really going to then work on creating an entirely new solution that overlaps tons of it, but hits that 2% place. We'd much rather figure out how to improve these areas to grow to meet that gap, rather than come up with an thing that has a lot of superfluousness and which itself will certainly add its own gaps :)

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Mar 7, 2022

Had you known this, you could have corrected me on it near the start of my griping.

I literally worked on that feature :)

The purpose of my posts was to show you that even if that feature did not exist, that that still is not a justification for the approach you were proposing.

Heck, let's do the thought experiment here. Say we didn't have using static SomeStaticClassWithExtensions. You are then insistent that we need struct-inheritance because it's massively important to you that any solution not come in through using SomeNamespace;. Again, the exact argument would be made that your dislike of using Namespace doesn't motivate defining this entirely different feature system for structs. Instead, we should have features (like what were added, and what have been proposed) to address that space, not reinventing some different space instead.

Again, even if we didn't have this feature, the counterpoints being made were valid. They did not depend on the existence of this happenign prior to your request. Indeed, that's a vital aspect of lang design. We have to consider htings that are potentially coming later. And we definitely will nto do something in the short term if it's going to be subsumed by something else we have strong belief we want to do. So, for the past, present and future, as long as we feel that extensions are an appropriate space, adn that we can continue on providing improvements on them to address pain points, then insisting that we build up a duplicative system against them just to address a tiny pain point you had, is not going to fly.

Finally, it's not my job to be aware of what you do or don't know. It took a long time just to even figure out waht you were asking for, given that the requirements kept changing (for example, if it should be a class or struct. if it's just adding members or not. if protected is available or not. if there's a single type at runtime or not). All these parts were both unclear and in flux from post to post. It takes time and effort to even try to condense down what issues it is you're having only to realize you don't know about certain lang features. This came up with object-initializers with things like collections/dictionary-init, and with extensible-enums where it turned otu a lot of your misinformation was based on errant views on performance that were not valid. Trying to detangle all the stuff which you do/don't know is non-trivial. :)

@najak3d
Copy link

najak3d commented Mar 7, 2022

Do you know if that "extensions always" feature proposal is being incorporated into "Roles/Extensions" feature, or is gaining traction? It seems like a really good idea.

#4029

@CyrusNajmabadi
Copy link
Member

Do you know if that "extensions always" feature proposal is being incorporated into "Roles/Extensions" feature, or is gaining traction? It seems like a really good idea.

@najak3d let's talk offline about that.

@najak3d
Copy link

najak3d commented Mar 7, 2022

@CyrusNajmabadi wrote: "Finally, it's not my job to be aware of what you do or don't know."

Are you seriously implying that you were aware of the ability to pull in extensions via class name? At the same point as you were telling me "that this is not necessary; we're not going to augment how extensions are included just for you".

It's blatant that you also were unaware of this, at least at the time you were arguing against me. I'm not saying "it wasn't in your memory bank at all; just that you had at minimum temporarily forgotten about it".

@TahirAhmadov
Copy link

To summarize, struct inheritance using different types cannot work - ever; struct "inheritance" the way @najak3d wants is basically roles - that's #5497. The only remaining topic is my "augmentation" idea - can somebody champion that one? Please thanks :)

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Mar 7, 2022

Are you seriously implying that you were aware of the ability to pull in extensions via class name?

Yes. You initially indicated you wanted an approach whereby only the new type was defined, and that consumers need not do anything to pick that up. Any and all associations with extensions were rejected by you as being entirely inferior. It was only later on that it appeared that you pivoted to potentially being ok with using if it only brought in the extensions from a single file. When it was pointed out that such a concept doesn't even make sense in C#, you pivoted one final time to stating that you woudl then be ok bringing in extensions from a particular class.

I cannot guess as to what you do or don't know. Nor can i wrap my head around the set of things you do or do not find acceptable. My experience so far has been that you have an internal list of things (for lack of a better word) that you just don't like and find distasteful (regardless of how the rest of the ecosystem may perceive them). THis list has not been coherent to me, and it constantly gets strange things added and shifts around as i learn about more things you don't like using.

Until now, i had no idea that you ahd an issue with usings. And my only internalization was that it was uniformly bad, and that you would reject any solution down that path for the same reason you rejected the using Namespace approach.

Again, i'm not a mind reader here. I'm having ot adjust to the changing arugment and requirements you're coming up with as you're telling us about them.

At the same point as you were telling me "that this is not necessary; we're not going to augment how extensions are included just for you".

I said we were not going to take an entirely alternative way to effectively extend types just because you didn't like that bringing in extensions through a using-statement can pull in tons of things. That remains true. Roles/Shapes will most likely follow the way we bring in extensions today. And if we feel like we need better ways to bring in extensions, we'll just do that, instead of investing some entirely different parallel stack for accomplishing the same. Community members have raised good ideas on how to do these things, and i think an approach of Extensions/Rolse + Those ideas is much better than building something wholely new and different for the narrow case of adding some members only to structs.

To your direct point, i was the person that literally linked you to the proposal that i felt exactly matched what you were asking for. Namely a no-edit way to pull in extensions for a type. The existing capabilities and whatnot did not seem to eb what you were asking for since you still didn't want any usings at all (and i presumed that that counted global-usings which would be even more problematic since they would apply to everything in your compilation).

@CyrusNajmabadi
Copy link
Member

The only remaining topic is my "augmentation" idea - can somebody champion that one? Please thanks :)

I'd recommend actually trying to start a discussion with that narrow scope, explaining what it is you want and what you don't. I'll tell you that if it's in line with the fork of hte convo you and i were having above, then i'm highly likely to champion it as i think it would be quite nice to have.

@TahirAhmadov
Copy link

The only remaining topic is my "augmentation" idea - can somebody champion that one? Please thanks :)

I'd recommend actually trying to start a discussion with that narrow scope, explaining what it is you want and what you don't. I'll tell you that if it's in line with the fork of hte convo you and i were having above, then i'm highly likely to champion it as i think it would be quite nice to have.

You say jump, I ask how high.

@najak3d
Copy link

najak3d commented Mar 8, 2022

@CyrusNajmabadi - I specifically said that extensions wouldn't be so bad if you could pull them in by class name instead of namespace, and you said "we're not going to change this for you; it's fine as is". This blatantly implies that what I was hoping for was "not a thing yet" -- but it was. If you had instead said "we're in luck, it already does work that way" -- it would have changed the conversation sooner. Luckily @HaloFour was reading, and informed us that it does already work (as I believe you should have recognized yourself, being that you are an expert). We all make mistakes, but it appears to me that you simply aren't willing to admit that you made this mistake. Not sure why...

So when "Roles/Extensions" adds support for extending "static members" on a struct -- that should suffice, especially now that I know this can be done in a fine-grained fashion -- class by class, instead of an entire namespace of extensions.

And you are right - I don't like a lot of "usings" -- apparently I'm not soo alone, given that for C# 10, they've made this easier to do via the "global using" notation. I also don't like abundant usings -- which clutter the intellisense with lots of stuff.

I often tend towards this notations:
using SD = System.Drawing;

rather than:
using System.Drawing;

If I'm only using 1-2 items from a namespace, I prefer the alias approach, because this way when code is cut/paste to another file, it doesn't mix-up "which Point" you wanted (there are several, all called "Point"). So "SD.Point" can never be misconstrued, and so that's what I tend to do, more times than not.

@najak3d
Copy link

najak3d commented Mar 8, 2022

I meander on what I want, because part of "knowing what I want" requires "knowing what is already available" as well as "what is difficult to achieve" and "what things open a can or worms of risks", etc.. I'm learning quickly as I go here.

Thus I meander, as I should. I'm not firm on what I want, because of new information that comes along.

And my focus isn't "what I want", but rather when I see things that I think "many would want" because these are related to issues I've seen for years.

@Xyncgas
Copy link

Xyncgas commented Jul 11, 2022

In C, we can use two different struct :

struct A
{
        long Id;
        long port;
}
struct B
{
        long Id;
        long port;
        int Secret;
}

And it's gonna work the same

B Obj = new B ();
MethodUsingA(&Obj );

Maybe we can have inheritance as long we are clear that struct that inheirts struct essentially put that struct on top of the struct in explicit layout

@theunrepentantgeek
Copy link

How do you handle issues of decapitation, especially when the value gets boxed onto the heap?

IIRC, C handles it by ignoring the issue, opening fertile ground for errors, memory leaks, out of bounds memory access, and other goodies of similar ilk.

@HaloFour
Copy link
Contributor

@theunrepentantgeek

IIRC, C handles it by ignoring the issue, opening fertile ground for errors, memory leaks, out of bounds memory access, and other goodies of similar ilk.

This is precisely why C# forbids it, and it's incredibly unlikely that the C# team would consider enabling an entire class of bug for a tiny bit of convenience here.

@Xyncgas
Copy link

Xyncgas commented Jul 12, 2022

@HaloFour But it's high performance computing, we put the entire thing in the box, it's evidential via sizeof () which struct you are dealing with and what you can do with you, for one you won't access places you aren't supposed to, while the default behavior of upcasting in c# with classes is legal it should also be safe when upcasting struct because you are visiting region of the struct with guarantees that it's contained (within the struct), meanwhile downcasting will be illegal as it always has been

That being said I am very comfortable with not having inheritance at all because I can already cast a struct to another struct by defining an operator that uses parts of the struct which is built via explicit layout (e.g. Layout.Point2D)

@TahirAhmadov
Copy link

How do you handle issues of decapitation, especially when the value gets boxed onto the heap?

IIRC, C handles it by ignoring the issue, opening fertile ground for errors, memory leaks, out of bounds memory access, and other goodies of similar ilk.

Please see my proposal here: #5897
Yes, in short, you can't "cast" structs - there is no way to accomplish it in .NET and will never be possible. However, I think we can have the next best thing - special type of "inheritance" for structs which solves code reuse and helps in most other use cases. Boxing the child and unboxing into a base is theoretically possible, but would require an expensive run-time change (not likely to happen), or modifying what the C# cast operator does (not likely to happen), or using a special helper method which would accomplish it (this is relatively easy).

@jnm2 jnm2 converted this issue into discussion #6270 Jul 12, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests