Skip to content
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: primary constructors #9517

Closed
orthoxerox opened this issue Mar 6, 2016 · 12 comments
Closed

Proposal: primary constructors #9517

orthoxerox opened this issue Mar 6, 2016 · 12 comments

Comments

@orthoxerox
Copy link
Contributor

To introduce positional deconstruction and bridge the gap between records and full-scale classes, I propose the addition of primary constructors.

Primary constructors for structs and standalone classes

A class with a primary constructor looks like this:

//This is a record class;
class ClassName1<T1, T2, ...>(TypeName1 parameterName1, TypeName2 parameterName2, ...);

//This is a class with a primary constructor
class ClassName2<T1, T2, ...>(TypeName1 parameterName1, TypeName2 parameterName2, ...)
{
    //more fields
    //more properties
    //more constructors
    //more methods
}

What does a primary constructor do?

  • it defines a readonly auto property for each parameter. The name is derived from the parameter name, but can be overridden with an attribute for unusual casing rules (uiModel -> UIModel)
  • it defines a constructor for the class/struct that sets the properties
  • it overrides Equals(object), GetHashCode(), IEquatable<T>.Equals(T) and ToString(), unless they are defined in the class/struct body
  • it defines a wither and a positional deconstructor for the same set of parameters, unless they are defined in the class/struct body

What does a primary constructor require from the user's code?

  • (class only) additional fields and properties must have an initializer if they are readonly
  • additional constructors must always ultimately call the primary constructor

Primary constructors for class hierarchies

A class with a primary constructor can inherit from a class with a primary constructor:

//This is a record class that strictly extends its parent;
class Person(string firstName, string lastName);
class Student(string firstName, string lastName, [PropertyName("GPA")] double gpa) : Person;

//This is a record class that restricts its parent;
class Ellipse(PointD focus1, PointD focus2, double majorAxis);
class Circle(PointD center, double radius)
    : Ellipse(center, center, radius*2);

//This is a class with a primary constructor that inherits from a class with a primary constructor
class DerivedName3<T1, T2, ...>(TypeName1 parameterName1, TypeName2 parameterName2, ...)
    : BaseName3 //since the argument list is omitted, C# must ensure that
                //this primary constructor strictly extends the base primary constructor
{
    //more fields
    //more properties
    //more constructors, all of them must call the primary constructor of this class
    //more methods
}

A class wthout a primary constructor can inherit from a class with a primary constructor:

class DerivedName3<T1, T2, ...>
    : ClassName3
{
    //fields
    //properties
    //constructors must call the base constructor
    //methods
}

A class with a primary constructor can inherit from a class without one only if the base class has a parameterless constructor.

An example of primary constructor expansion done by the compiler

//Original code
class Ellipse(PointD focus1, PointD focus2, double majorAxis)
{
    public double MinorAxis => 2 * Math.Sqrt(MajorAxis*MajorAxis/4 - PointD.DistanceSquared(Focus1, Focus2)/4);
}
class Circle(PointD center, double radius)
    : Ellipse(center, center, radius*2);
//Expanded code
class Ellipse : IEquatable<Ellipse>
{
    public PointD Focus1 { get; }
    public PointD Focus2 { get; }
    public double MajorAxis { get; }
    public double MinorAxis => 2 * Math.Sqrt(MajorAxis*MajorAxis/4 - PointD.DistanceSquared(Focus1, Focus2)/4);

    public Ellipse(
        PointD focus1, 
        PointD focus2, 
        double majorAxis)
    {
        Focus1 = focus1;
        Focus2 = focus2;
        MajorAxis = majorAxis;
    }

    public override string ToString()
        => $"Ellipse(Focus1={Focus1}, Focus2={Focus2}, MajorAxis={MajorAxis})";

    public void GetValues(
        out PointD focus1 = this.Focus1, 
        out PointD focus2 = this.Focus2,
        out double majorAxis = this.MajorAxis)
    {
        return;
    }

    public Ellipse With(
        PointD focus1 = this.Focus1, 
        PointD focus2 = this.Focus2,
        double majorAxis = this.MajorAxis)
        => new Ellipse(focus1, focus2, majorAxis);

    //Equals, GetHashCode ...
}

class Circle : Ellipse, IEquatable<Circle>
{
    public PointD Center { get; }
    public double Radius { get; }

    public Circle(
        PointD center,
        double radius)
        : base(center, center, radius*2)
    {
        Center = center;
        Radius = radius;
    }

    public override string ToString()
        => $"Circle(Center={Center}, Radius={Radius})";

    public void GetValues(
        out PointD center = this.Center, 
        out double radius = this.Radius)
    {
        return;
    }

    public Circle With(
        PointD center = this.Center, 
        double radius = this.Radius)
        => new Circle(center, radius);


    //Equals, GetHashCode ...
}
@alrz
Copy link
Member

alrz commented Mar 6, 2016

I believe primary constructors were proposed before and currently spec draft for records does have an optional class body so you can define other members in it.

@orthoxerox
Copy link
Contributor Author

@alrz I cannot find the record spec in https://github.com/dotnet/roslyn/tree/future/docs/features

@alrz
Copy link
Member

alrz commented Mar 6, 2016

@orthoxerox It's proposed in #206 in the same form that you've presented here.

@orthoxerox
Copy link
Contributor Author

@alrz looks like it has been excluded from the working draft in the sources. I would've said "great minds think alike", but this proverb is much less flattering in Russian.

@alrz
Copy link
Member

alrz commented Mar 6, 2016

FYI, #4481, #6997

@Miista
Copy link

Miista commented Mar 13, 2016

Instead of deriving the name for the auto property, why not just use the name as it is?
This would avoid the whole thing with unusual casing rules.
It would also abstract over the distinction between ctor parameters and auto properties.

Instead of

class Person(string firstName, string lastName);

You would have

class Person(string FirstName, string LastName);

@orthoxerox
Copy link
Contributor Author

@Miista that will simplify a lot of internal plumbing for records, but every single FxCop in existence will throw a tantrum when it sees the resulting assembly.

@Miista
Copy link

Miista commented Mar 14, 2016

And that is a problem because?

@paulomorgado
Copy link

Because written code is a social interaction between code writers and FxCop and StyleCop validate the rules of engagement, @Miista.

@Miista
Copy link

Miista commented Mar 14, 2016

I just don’t see why FxCop or StyleCop should dictate how the language should evolve. Shouldn't FxCop and StyleCop adapt to how C# evolves, not the other way around?

@alrz
Copy link
Member

alrz commented Mar 14, 2016

I think something like this were proposed,

sealed class Person(string firstName : FirstName, string lastName : LastName);

But I didn't see anything like that recently, perhaps it's discarded and the following is the accepted convention,

sealed class Person(string FirstName, string LastName);

To be honest, it's overkill to write a record like that and repeat every member's name twice.

@orthoxerox
Copy link
Contributor Author

Closed in favor of #10154

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants