Proposal: Boilerplate feature to invoke boilerplate code #1832
-
I've noticed implementing IComparable and IEquatable can land you with a lot of such code: namespace MyNamespace
{
public class MyType : IEquatable<MyType>, IComparable<MyType>
{
public int CompareTo(MyType other)
{
...
}
public virtual bool Equals(MyType other)
{
...
}
public static bool operator >(MyType lhs, MyType rhs) => lhs.CompareTo(rhs) > 0;
public static bool operator <(MyType lhs, MyType rhs) => lhs.CompareTo(rhs) < 0;
public static bool operator >=(MyType lhs, MyType rhs) => lhs.CompareTo(rhs) >= 0;
public static bool operator <=(MyType lhs, MyType rhs) => lhs.CompareTo(rhs) <= 0;
public static bool operator ==(MyType lhs, MyType rhs)
{
if (lhs is null && rhs is null)
return true;
else if (lhs is null)
return false;
else
return lhs.Equals(rhs);
}
public static bool operator !=(MyType lhs, MyType rhs) => !(lhs == rhs);
}
} So I propose a generic The boilerplate is not a contract and not inherited; it behaves as if the code were inserted directly into the class. Example: using Boilerplate;
namespace Boilerplate
{
public boilerplate BCompareOperators<T>
{
bool operator >(T lhs, T rhs) => lhs.CompareTo(rhs) > 0;
bool operator <(T lhs, T rhs) => lhs.CompareTo(rhs) < 0;
bool operator >=(T lhs, T rhs) => lhs.CompareTo(rhs) >= 0;
bool operator <=(T lhs, T rhs) => lhs.CompareTo(rhs) <= 0;
}
public boilerplate BEqualsOperator<T>
{
bool operator ==(T lhs, T rhs)
{
if (lhs is null && rhs is null)
return true;
else if (lhs is null)
return false;
else
return lhs.Equals(rhs);
}
}
public boilerplate BNotEqualsOperator<T>
{
bool operator !=(T lhs, T rhs) => !(lhs == rhs);
}
}
namespace MyNamespace
{
public class MyType : IEquatable<MyType>, IComparable<MyType>
{
with public static boilerplate BCompareOperators<MyType>;
with public static boilerplate BEqualsOperator<MyType>;
with public static boilerplate BNotEqualsOperator<MyType>;
public int CompareTo(MyType other)
{
...
}
public virtual bool Equals(MyType other)
{
...
}
}
} There are several advantages here, including the ability for boilerplate methods to access private fields and properties. Boilerplates aren't code contracts, and carry the same implications to clients and derived types as typing the boilerplate code into the class. This leaves a few questions, of course:
Consolidating the code into its own assembly restricts it with rules similar to a base class: changes to boilerplate may change types using the boilerplate. By contrast, putting boilerplate inline—or as local to the assembly or module—means standard boilerplate is nonsense, and all boilerplate is included in each individual assembly. |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments
-
This sounds a bit like #1014 and joins the list of things that could probably be solved with #107. |
Beta Was this translation helpful? Give feedback.
-
I was going to say that it sounds like traits, and default interface methods will bring this support to the table, at least for instance members. For static members you'd need something additional, but maybe it can build on top of interfaces/traits rather than being something completely new. With DIM we should also see support for static members on interfaces, but they're not "inherited" by implementing types. If there was a language feature to "copy" those members onto the implementing type I think that would satisfy this proposal. public interface IEquatable<T> {
bool Equals(T other);
public static bool operator ==(IEquatable<T> left, IEquatable<T> right) {
return left != null ? left.Equals(right) : right == null;
}
public static bool operator !=(IEquatable<T> left, IEquatable<T> right) {
return left != null ? !left.Equals(right) : right != null;
}
}
public class Foo with IEquatable<Foo> {
// compiler automatically adds these members:
public static bool operator ==(IEquatable<T> left, IEquatable<T> right) {
return IEquatable<T>.op_Equality(left, right);
}
public static bool operator !=(IEquatable<T> left, IEquatable<T> right) {
return IEquatable<T>.op_Inequality(left, right);
}
} |
Beta Was this translation helpful? Give feedback.
-
You're example syntax is really robust and boilerplate itself. Why not just: with BCompareOperators<MyType>;
with BEqualsOperator<MyType>;
with BNotEqualsOperator<MyType>; Instead of: with public static boilerplate BCompareOperators<MyType>;
with public static boilerplate BEqualsOperator<MyType>;
with public static boilerplate BNotEqualsOperator<MyType>; I can see wanting to make to control it being |
Beta Was this translation helpful? Give feedback.
-
I'm not sure what to make of #104 (I can't quite tell what it does or what it's trying to accomplish). As for why it's not default interface methods? That's because it's not an interface, and not a contract. this is blunt, verbatim code. It can be private, not part of interfaces. It can include fields. A class doesn't have BCompareOperators as any particular attribute: if you were to enter those blocks of code raw, by hand, the result would be the same. As for why the public static etc. options, I'm not certain. I initially considered sticking those definitions inside the blocks themselves, and then said, "Hey, what if I want these methods to have a different definition, or want them to be virtual, sealed, new, or override? That's a reasonable thing to do differently on different classes in an inheritance relationship. Do I have two separate copies, or the same code once and just put the keywords before the call to inline it?" This is basically just a fancy version of inline functions. |
Beta Was this translation helpful? Give feedback.
-
@bluefoxicy
That proposal is all about auto-generation of boilerplate code where that code is provided by libraries. A "generator", which is akin to an "analyzer", would be invoked during compilation and could scan the syntax trees and emit portions of source that would then be included in the compilation. It has a much higher bar to entry given that a developer would have to write this "generator" using Roslyn's APIs but it would also give that "generator" a lot more control over what source is generated. For example, you could add a project reference to some library containing generators for equality, then write: [Equatable]
public class MyType : IEquatable<MyType> { } and during compilation the generator would emit: public partial class MyType {
bool IEquatable<MyType>.Equals(MyType other) { ... }
public static bool operator ==(MyType left, MyType right) { ... }
public static bool operator !=(MyType left, MyType right) { ... }
}
Interfaces will also get
But not fields, they can have no state due to issues with multiple inheritance. Any proposal like this would also likely have to deal with those same problems.
To me it sounds a lot more like composition. Something that could be built on traits/mixins/delegation. Other template/macro proposals haven't gained a whole lot of traction here. There are lots of ambiguity and collision issues that potentially arise with those kinds of solutions. I like the idea, just not some of the details. I don't like the name |
Beta Was this translation helpful? Give feedback.
-
Every problem in computer science can be solved by adding another level of abstraction: you could write a boilerplate generator, which would then allow others to write and use boilerplate classes. E.g.: [BoilerplateDefinition]
public class BEqualsOperator<T>
{
public static bool operator ==(T lhs, T rhs)
{
…
}
public static bool operator !=(T lhs, T rhs)
{
…
}
}
[Boilerplate(typeof(BEqualsOperator<MyType>))]
public class MyType : IEquatable<MyType>
{
public virtual bool Equals(MyType other)
{
…
}
} (Okay, the |
Beta Was this translation helpful? Give feedback.
-
This can pretty much be solved using C# 9.0 source generators. |
Beta Was this translation helpful? Give feedback.
This can pretty much be solved using C# 9.0 source generators.