How we got bored of boilerplate and added new features to C# to save time.
Artūras Šlajus
2023
Unity Version | C# for Unity | C# for .NET |
---|---|---|
2021.2+ | 9.0 | 10.0 |
2020.2+ | 8.0 | 9.0 |
2018.3+ | 4 / 7.3 | 7.3 |
[Record]
public partial class NumParam : StringTemplateParam {
/// <inheritdoc cref="VarName"/>
public override VarName varName { get; }
[LazyProperty]
public override NonEmpty<ImmutableArrayC<ParamValue.Type>>
supportedTypes =>
NonEmpty.arrayC(ParamValue.Type.Numeric);
/// <inheritdoc cref="Inflections"/>
public readonly Inflections inflections;
/// <inheritdoc cref="Inflector"/>
public readonly Inflector inflector;
}
[Matcher] public interface ITicksError {}
public static partial class TicksError {
/// <summary>Error that is expected and can be handled.</summary>
[Record] public sealed partial class Expected : ITicksError {
public readonly string error;
}
/// <summary>Error that is unexpected and should not have happened.</summary>
[Record] public sealed partial class Unexpected : ITicksError {
public readonly string error;
}
}
IValidateMatchJobHandlerResult onTicksParsingError(
RedisStoredData redisData, Replay.ITicksError error
) => error.matchM(
expected: expected =>
ValidateMatchJobHandlerResult.Ignore.instance,
unexpected: unexpected =>
new ValidateMatchJobHandlerResult.FatalError(
"Received redis data for had an unexpected error: " +
unexpected
)
);
using GenerationAttributes;
public static A Sum<A>(
this IEnumerable<A> elements,
[Implicit] Numeric<A> numeric = default
) =>
elements.Aggregate(
numeric.Zero,
(a, b) => numeric.Add(a, b)
);
When you invoke this function, you can either:
Provide the argument explicitly, then it will behave as a regular function.
var sum = new []{1, 2, 3}.Sum(Numeric.Integer);
var sum = new []{1, 2, 3}.Sum();
The compiler first looks for any parameters marked as [Implicit]
in the local function.
For example:
public int SumIntegers(
int[] integers,
[Implicit] Numeric<int> numeric = default
) =>
integers.Sum();
Will compile to:
public int SumIntegers(
int[] integers,
[Implicit] Numeric<int> numeric = default
) =>
integers.Sum(numeric);
If the compiler can not find a suitable value in the local scope, it tries the class / struct scope.
For example:
class NumberCruncher {
// This can also be static.
[Implicit] readonly Numeric<int> IntNumeric = /* ... */;
public int SumIntegers(int[] integers) => integers.Sum();
}
Will compile to:
class NumberCruncher {
[Implicit] readonly Numeric<int> IntNumeric = /* ... */;
public int SumIntegers(int[] integers) => integers.Sum(IntNumeric);
}
If class / struct scope fails as well, it tries to find a public static
field, property or method to provide the value.
For example:
public static class Numerics {
[Implicit] public static readonly Numeric<int> IntNumeric = /* ... */;
}
class NumberCruncher {
public int SumIntegers(int[] integers) => integers.Sum();
}
Will compile to:
public static class Numerics {
[Implicit] public static readonly Numeric<int> IntNumeric = /* ... */;
}
class NumberCruncher {
public int SumIntegers(int[] integers) =>
integers.Sum(Numerics.IntNumeric);
}
If it can not find a value, a compilation error is emitted:
Example.cs(24, 16): [Implicits02] No matching implicits found for parameter 'numeric' of type 'Numeric<int>' on operation 'SumIntegers'
interface MyInterface {
int GetAge(Person person);
}
[DelegateToInterface(
delegatedInterface = typeof(MyInterface),
delegateTo = nameof(implementer)
)]
public partial class TestClass {
public MyInterface implementer;
}
public partial class TestClass : MyInterface {
public int GetAge(Person person) =>
implementer.GetAge(person);
}
public abstract partial class ToBeMixedIn {
/// <summary>publicField</summary>
public int publicField = 8;
/// <summary>protectedField</summary>
protected int protectedField = 5;
/// <summary>privateField</summary>
[SerializeField] int privateField = 4;
}
[Mixin(typeToMixin: typeof(TypeToMixIn))]
public partial class ToBeMixedInto {}
public enum TestEnum { Int1, Str, Int2 }
// If you have duplicate case types, you must specify the
// enum type.
[Union(
cases: new []{typeof(int), typeof(string), typeof(int)},
caseEnumType: typeof(TestEnum)
)]
public partial class UnionTest2 {
public static void test() {
default(UnionTest2).voidFoldM(
onInt1: i => Console.WriteLine(i),
onString: str => str.Clone(),
onInt2: a => {
Console.WriteLine(a);
}
);
}
}
[SubsetEnum(
name: "ADEnum", docSummary: "test1",
cases: new[]{nameof(A), nameof(D)}
)]
public enum FullEnum : byte {
A, B, C, D
}
public enum BuildRegularType {
Development = 0, Stage = 1, Production = 2
}
public enum BuildSpecialType {
MapEditor = 100
}
[EnumUnion(toMerge: new[] {
typeof(BuildRegularType),
typeof(BuildSpecialType)
})]
public static partial class BuildType {}
[LambdaInterface] interface IPartB {
string name { get; }
int sharedNumber { get; }
void doSomethingElse(bool a, bool b);
void shared(bool a);
}
public sealed class LambdaIPartB : IPartB {
public string name { get; }
public int sharedNumber { get; }
public readonly System.Action<bool, bool>
_doSomethingElse;
public void doSomethingElse(bool a, bool b) =>
_doSomethingElse(a, b);
public readonly System.Action<bool> _shared;
public void shared(bool a) => _shared(a);
}
[TypeSafeEnumWrapper(
typeWithConstants = typeof(RewiredConsts.Action),
enumUnderlyingType = typeof(int)
)]
public readonly partial struct RewiredActionT {
public override string ToString() =>
$"{Macros.classShortName}({value})";
}
public class ClassMutable {
public int x;
public string foo;
public IList<string> fooOverriden;
}
[ReadOnlyMirror(
of = typeof(ClassMutable),
overridenFieldNames = new []{
nameof(ClassMutable.fooOverriden)
},
overridenFieldTypes = new []{
typeof(List<string>)
}
)]
public sealed partial class ClassImmutable {}
Macros are programs that run on the compilation time.
We currently have 3 types of macros:
public static class AnyExts {
/// <summary>
/// Returns "expression=evaluated expression" string.
/// </summary>
[ExpressionMacro(@"(""{{a}}="" + ({{a}}))")]
public static string echo<A>(this A a) =>
throw new MacroException();
}
var error =
$"No such {index.echo()}. {list.Count.echo()}";
var error =
$"No such {("index=" + (index))}. " +
$"{("list.Count=" + list.Count)}";
var error =
$"No such index={index}. list.Count={list.Count}";
public static class NullableExts {
[StatementMacro(@"
var {{ uniqueId }} = {{ opt }};
if (({{ uniqueId }}).HasValue) {
{{ inline 'ifSome' uniqueId + '.Value' }};
} else {
{{ inline 'ifNone' }};
}
")]
public static void voidFoldM<A>(
this A? opt, Action ifNone, Action<A> ifSome
) =>
throw new MacroException();
}
void run(int? maybeValue) {
maybeValue.voidFoldM(
ifNone: () => {
Console.WriteLine("No value.")
},
ifSome: value => {
Console.WriteLine($"Received value: {value}");
}
);
}
void run(int? maybeValue) {
var id_542_671 = maybeValue;
if ((id_542_671).HasValue) {
_LOCAL_ifSome_601_670(id_542_671.Value);
}
else {
Console.WriteLine("No value.");
}
void _LOCAL_ifSome_601_670(int value) {
Console.WriteLine($"Received value: {value}");
}
}
public readonly struct Result<TValue, TError> {
[VarMacro(@"
var {{ uniqueId }} = {{ self }};
if ({{ uniqueId }}.IsError)
return {{ uniqueId }}.__UnsafeError;
{{ varType }} {{ varName }} =
{{ uniqueId }}.__UnsafeValue;
")]
public TValue ValueOr_RETURN() =>
throw new MacroException();
public static implicit operator Result<TValue, TError>(
TError error
) => new(error);
public static implicit operator Result<TValue, TError>(
TValue value
) => new(value);
}
Result<int, string> AddResults(
Result<int, string> num1Result,
Result<int, string> num2Result
) {
var num1 = num1Result.ValueOr_RETURN();
var num2 = num2Result.ValueOr_RETURN();
return num1 + num2;
}
static Result<int, string> AddResults(
Result<int, string> num1Result,
Result<int, string> num2Result
) {
var id_1136_1163 = num1Result;
if (id_1136_1163.IsError)
return id_1136_1163.__UnsafeError;
int num1 = id_1136_1163.__UnsafeValue;
var id_1181_1208 = num2Result;
if (id_1181_1208.IsError)
return id_1181_1208.__UnsafeError;
int num2 = id_1181_1208.__UnsafeValue;
return num1 + num2;
}
[
AttributeMacro(@"
public {{
if field.is_static
'static'
end
}} void EDITOR_set{{ field.name | rename | pascal_case }}(
{{ field.type | type_reduced_name }} value
) => {{ field.name }} = value;
"),
AttributeUsage(AttributeTargets.Field)
]
public class EditorSetterAttribute : Attribute {}
class MyCharacterData {
[SerializeField, EditorSetter] CharacterRole _type;
}
[
AttributeMacro(@"
public static TimeSpan {{ ourMethodBaseName }}(this int v) =>
TimeSpan.{{ timeSpanMethodName }}(v);
public static TimeSpan {{ ourMethodBaseName }}s(this int v) =>
TimeSpan.{{ timeSpanMethodName }}(v);
"),
AttributeUsage(
AttributeTargets.Class, AllowMultiple = true
)
]
class GenTimeSpanHelpersAttribute : Attribute {
public string ourMethodBaseName;
public string timeSpanMethodName;
}
[
GenTimeSpanHelpers(
ourMethodBaseName = "milli",
timeSpanMethodName = nameof(TimeSpan.FromMilliseconds)
),
GenTimeSpanHelpers(
ourMethodBaseName = "second",
timeSpanMethodName = nameof(TimeSpan.FromSeconds)
),
GenTimeSpanHelpers(
ourMethodBaseName = "minute",
timeSpanMethodName = nameof(TimeSpan.FromMinutes)
),
GenTimeSpanHelpers(
ourMethodBaseName = "hour",
timeSpanMethodName = nameof(TimeSpan.FromHours)
),
GenTimeSpanHelpers(
ourMethodBaseName = "day",
timeSpanMethodName = nameof(TimeSpan.FromDays)
),
]
public static partial class TimeSpanExts {}