Created
May 7, 2024 14:42
-
-
Save bartdesmet/cd03b104832cf1f4616fe205cdabc737 to your computer and use it in GitHub Desktop.
C# Language Internals @ Techorama 2024
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Immutable; | |
using System.Numerics; | |
using System.Runtime.CompilerServices; | |
internal class Program | |
{ | |
static void Main(string[] args) => Console.WriteLine("C# Language Internals @ Techorama 2024"); | |
} | |
class CSharp7 | |
{ | |
class TupleTypes | |
{ | |
// | |
// How does the metadata for the names of tuple elements get stored? | |
// | |
// [return: TupleElementNames(new string[] { "a", "b", null, "g", "c", null, "f", "d", "e" })] | |
static (int a, int b, (int c, (int d, int e), int f), int g) Demo() => default; | |
// [return: TupleElementNames(new string[] { "a", "b" })] | |
static List<(int a, int b)> Demo2() => default; | |
} | |
class MoreMetadata | |
{ | |
// | |
// Storing metadata to annotate types isn't new, e.g. C# 4.0 dynamic. | |
// | |
// [return: Dynamic] | |
static dynamic Foo() => default; | |
// [return: Dynamic(new bool[] { false, false, false, true })] | |
static Dictionary<string, List<dynamic>> Bar() => default; | |
// | |
// And also for nullable reference types in C# 8.0. | |
// | |
// [return: Nullable(new byte[] { 1, 2 })] | |
static List<string?> Qux(string[] xs) => default; | |
} | |
class LocalFunctions | |
{ | |
// | |
// This generates a static method: | |
// | |
// static int <Static>g__F|0_0() | |
// | |
static void Static() | |
{ | |
int x = F(); | |
int F() => 42; | |
} | |
// | |
// This generates a struct-based closure that's passed by-ref: | |
// | |
// static int <Closure>g__F|1_0(ref <>c__DisplayClass1_0 P_0) | |
// | |
static void Closure() | |
{ | |
int x = 42; | |
int y1 = F(); | |
int y2 = F(); | |
int F() => x++; | |
} | |
// | |
// This simply constructs a delegate to the generated static method: | |
// | |
// return <StaticHoist>g__F|2_0; | |
// | |
static Func<int> StaticHoist() | |
{ | |
int F() => 42; | |
return F; | |
} | |
// | |
// This generates a class-based closure with a generated instance method: | |
// | |
// <>c__DisplayClass3_0<> c__DisplayClass3_ = new <> c__DisplayClass3_0(); | |
// <>c__DisplayClass3_.x = 42; | |
// return <>c__DisplayClass3_.<ClosureHoist>g__F|0; | |
// | |
static Func<int> ClosureHoist() | |
{ | |
int x = 42; | |
int F() => x++; | |
return F; | |
} | |
} | |
class PatternMatching | |
{ | |
// | |
// Each `is` expression expands to a set of Boolean checks, | |
// with possible repeated re-evaluations. | |
// | |
static string Is(object o) | |
{ | |
if (o is null) // if (o == null) | |
{ | |
return "null"; | |
} | |
else if (o is 42) // if (o is int && (int)o == 42) | |
{ | |
return "42"; | |
} | |
else if (o is int x) // if (o is int) | |
{ | |
return x.ToString(); // return ((int)o).ToString(); | |
} | |
else | |
{ | |
return "other"; | |
} | |
} | |
// | |
// Here the compiler can reason across different `case`s. | |
// | |
static string Switch(object o) | |
{ | |
//if (o != null) | |
//{ | |
// if (o is int) | |
// { | |
// int x = (int)o; // Note there's only one `int` check and cast. | |
// if (x == 42) | |
// { | |
// return "42"; | |
// } | |
// return x.ToString(); | |
// } | |
// return "other"; | |
//} | |
//return "null"; | |
switch (o) | |
{ | |
case null: | |
return "null"; | |
case 42: | |
return "42"; | |
case int x: | |
return x.ToString(); | |
default: | |
return "other"; | |
} | |
} | |
} | |
} | |
class CSharp8 | |
{ | |
class NullableReferenceTypes | |
{ | |
// | |
// Metadata is used to indicate where the `?` annotations are: | |
// | |
// 1 = non-null | |
// 2 = nullable | |
// | |
static void F(string s) { } | |
static void F(string[] s) { } | |
// static void F2([Nullable(new byte[] { 2, 1 })] string[] s) | |
static void F2(string[]? s) { } | |
// [NullableContext(2)] | |
static void G(string? s) { } | |
// static void G([Nullable(new byte[] { 1, 2 })] string[] s) | |
static void G(string?[] s) { } | |
// [NullableContext(2)] | |
static void G2(string?[]? s) { } | |
} | |
class IndexAndRange | |
{ | |
// | |
// Indexes are converted to int-based index values. | |
// | |
// xs[i.GetOffset(xs.Length)] | |
static int ArrayByIndex(int[] xs, Index i) => xs[i]; | |
// xs[i.GetOffset(xs.Count)] | |
static int ListByIndex(List<int> xs, Index i) => xs[i]; | |
// s[i.GetOffset(s.Length)] | |
static char StringByIndex(string s, Index i) => s[i]; | |
// | |
// From-end expressions turn into `Index` values with `FromEnd` set. | |
// This gets lowered further to an int-based index value. | |
// | |
// xs[xs.Length - 1] | |
static int ArrayFromEnd(int[] xs) => xs[^1]; | |
// xs[xs.Count - 1] | |
static int ListFromEnd(List<int> xs) => xs[^1]; | |
// s[s.Length - 1] | |
static char StringFromEnd(string s) => s[^1]; | |
// | |
// Ranges construct new instances using runtime helper methods, | |
// or using well-known methods like `Slice` and `Substring`. | |
// | |
// RuntimeHelpers.GetSubArray(xs, Range.All) | |
static int[] ArrayRangeAll(int[] xs) => xs[..]; | |
// xs.Slice(0, xs.Count) | |
static List<int> ListRangeAll(List<int> xs) => xs[..]; | |
// s.Substring(0, s.Length) // Note this returns `s` itself. | |
static string StringRangeAll(string s) => s[..]; | |
// RuntimeHelpers.GetSubArray(xs, Range.EndAt(new Index(1, true))) | |
static int[] ArraySuffix(int[] xs) => xs[..^1]; | |
// xs.Slice(0, xs.Count - 1) | |
static List<int> ListSuffix(List<int> xs) => xs[..^1]; | |
// s.Substring(0, s.Length - 1) | |
static string StringSuffix(string s) => s[..^1]; | |
// RuntimeHelpers.GetSubArray(xs, Range.StartAt(1)) | |
static int[] ArrayPrefix(int[] xs) => xs[1..]; | |
// xs.Slice(1, xs.Count - 1) | |
static List<int> ListPrefix(List<int> xs) => xs[1..]; | |
// s.Substring(1, s.Length - 1) | |
static string StringPrefix(string s) => s[1..]; | |
// RuntimeHelpers.GetSubArray(xs, new Range(1, new Index(1, true))) | |
static int[] ArraySlice(int[] xs) => xs[1..^1]; | |
// xs.Slice(1, xs.Count - 1 - 1) | |
static List<int> ListSlice(List<int> xs) => xs[1..^1]; | |
// s.Substring(1, s.Length - 1 - 1) | |
static string StringSlice(string s) => s[1..^1]; | |
} | |
class AsyncStreams | |
{ | |
static async IAsyncEnumerable<int> F(int begin, int end) | |
{ | |
// | |
// An async iterator has two possible suspensions: | |
// | |
// 1. `await` expressions | |
// 2. `yield return` statements | |
// | |
// A state machine is built to rule them all. Notable differences with | |
// regular async methods are: | |
// | |
// 1. Use of `ManualResetValueTaskSourceCore<bool> <>v__promiseOfValueOrEnd` | |
// to coordinate `yield return`, `yield break` (or end), and exceptions | |
// between producer (`MoveNext` state machine) and consumer (`MoveNextAsync`). | |
// 2. Use of `bool <>w__disposeMode` to track request to `DisposeAsync` and | |
// run the cleanup code path (e.g. `finally` blocks) when disposing. | |
// 3. Use of `int <>l__initialThreadId` akin to the synchronous iterator | |
// pattern where an `IEnumerable<T>` morphs into an `IEnumerator<T>` when | |
// enumeration is triggered on the same thread. Otherwise, a clone is made. | |
// | |
for (int i = begin; i < end; i++) | |
{ | |
await Task.Yield(); | |
yield return i; | |
} | |
} | |
static async Task Demo() | |
{ | |
// | |
// This desugars into: | |
// | |
// IAsyncEnumerator<int> asyncEnumerator = F(0, 10).GetAsyncEnumerator(); | |
// try | |
// { | |
// while (await asyncEnumerator.MoveNextAsync()) | |
// { | |
// Console.WriteLine(asyncEnumerator.Current); | |
// } | |
// } | |
// finally | |
// { | |
// if (asyncEnumerator != null) | |
// { | |
// await asyncEnumerator.DisposeAsync(); | |
// } | |
// } | |
// | |
// which could be abbreviated using `await using` as well. Further inspection | |
// into the `await` in `finally` (added in C# 6.0) could be done: | |
// | |
// 1. `finally` is turned into `catch (object ex) { ex_ = ex; }`. | |
// 2. The `ex_` variable ends up in the async state machine object. | |
// 3. Code in `finally` is run unconditionally. | |
// 4. The `ex_` variable is checked for `null`, and any exception is rethrown. | |
// | |
await foreach (var x in F(0, 10)) | |
{ | |
Console.WriteLine(x); | |
} | |
} | |
} | |
} | |
class CSharp9 | |
{ | |
class RecordTypes | |
{ | |
// | |
// Record types generate: | |
// | |
// 1. A class (or struct): | |
// `class Person` | |
// 2. Init-only properties: | |
// `string Name { get; init; }` | |
// 3. Implementation of `IEquatable<T>` delegating to the properties: | |
// a. Equals and chaining from `object.Equals`. | |
// b. GetHashCode using a multiple-add hash combine. | |
// 4. Operators `==` and `!=` chaining to `Equals`. | |
// 5. `Deconstruct` support: | |
// `void Deconstruct(out string Name, out int Age)` | |
// 6. `ToString` support: | |
// a. `ToString` override | |
// b. `PrintMembers(StringBuilder)` protected utility method. | |
// 7. A "copy constructor" pattern for use by `with`: | |
// a. `protected Person(Person original)` | |
// b. `virtual Person <Clone>$()` | |
// 8. Support for inheritance: | |
// a. The clone method is `virtual`. | |
// b. The `PrintMembers` functionality enables reuse by derived types. | |
// c. The `Equals` implementation uses an `Type EqualityContract`. | |
// | |
record Person(string Name, int Age); | |
// | |
// `with` expressions work for other types besides records. In case of | |
// record types, it uses the clone functionality: | |
// | |
// Person person = p.<Clone>$(); | |
// person.Age = p.Age + 1; | |
// | |
static Person Birthday(Person p) => p with { Age = p.Age + 1 }; | |
} | |
class CovariantReturnTypes | |
{ | |
abstract class Base | |
{ | |
public abstract object F(); | |
} | |
// | |
// A more derived return type can be used in an override. This | |
// required changes to the CLR. The generated C# code is: | |
// | |
// [PreserveBaseOverrides] | |
// public new virtual string F() | |
// | |
// with the following IL details: | |
// | |
// .override method instance object CSharp9/CovariantReturnTypes/Base::F() | |
// | |
// The new `PreserveBaseOverrides` attribute is used to ensure that | |
// virtual dispatch always calls the most-derived implementation, see | |
// https://github.com/dotnet/runtime/blob/main/docs/design/features/covariant-return-methods.md | |
// | |
class Derived : Base | |
{ | |
public override string F() => default; | |
} | |
} | |
class RelationalAndLogicalPatterns | |
{ | |
// | |
// Relational patterns simply become range checks. Note that a decompiler | |
// like ILSpy may get confused: | |
// | |
// switch (c) | |
// { | |
// case 'A': | |
// ... | |
// case 'Z': | |
// case 'a': | |
// ... | |
// case 'z': | |
// return true; | |
// } | |
// | |
// but the IL code is simply a few `bge` and `blt` branch instructions. | |
// | |
static bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; | |
} | |
} | |
class CSharp10 | |
{ | |
class InterpolatedStringHandlers | |
{ | |
// | |
// Enable efficient string interpolation to custom logger frameworks etc. A | |
// few highlights on the implementation are below. | |
// | |
enum LogLevel | |
{ | |
Info, | |
Warning, | |
Error, | |
} | |
// | |
// An instance method for logging, where interpolated strings can be assigned | |
// to parameter `s` of type `MyString`, which is an interpolated string handler. | |
// | |
// The use of `InterpolatedStringHandlerArgument` instructs the compiler to pass | |
// extra parameters to the constructor of `MyString`, in this case the `this` | |
// instance, and the `level` parameter, allowing the interpolated string handler | |
// to perform checks (e.g. don't do any formatting/allocations/etc. when the log | |
// level is not warranting producing output). | |
// | |
void Log(LogLevel level, [InterpolatedStringHandlerArgument("", nameof(level))] MyString s) | |
{ | |
} | |
void Demo(int x, int y) | |
{ | |
// | |
// This produces code to manufacture a MyString instance: | |
// | |
// MyString s = new MyString(10, 2, this, level); | |
// if (s.AppendLiteral("x = ") && s.AppendFormatted(x) && s.AppendLiteral(", y = ")) | |
// { | |
// s.AppendFormatted(y, "D"); | |
// } | |
// | |
// Note a few things: | |
// 1. Magic argument values `10` and `2` match: | |
// a. 10 is the literal string length, combined: | |
// $"x = {x}, y = {y:D}" | |
// 1234 56789A | |
// b. 2 is the number of interpolations: | |
// $"x = {x}, y = {y:D}" | |
// -1- --2-- | |
// 2. The `this` and `level` parameters are passed to the constructor. | |
// 3. Calls to `Append*` methods return `bool` (this is optional) and | |
// results are checked. This enables an interpolated string handler | |
// to stop the formatting early, e.g. when exceeding a length. | |
// | |
Log(LogLevel.Info, $"x = {x}, y = {y:D}"); | |
} | |
[InterpolatedStringHandler] | |
struct MyString | |
{ | |
public MyString(int literalLength, int interpolationCount, InterpolatedStringHandlers logger, LogLevel level) | |
{ | |
} | |
public bool AppendLiteral(string s) => true; | |
public bool AppendFormatted<T>(T value) => true; | |
public bool AppendFormatted<T>(T value, string format) => true; | |
} | |
} | |
class PropertyPatterns | |
{ | |
class Person | |
{ | |
public string Name { get; set; } | |
public int Age { get; set; } | |
public City City { get; set; } | |
} | |
class City | |
{ | |
public string Name { get; set; } | |
} | |
// | |
// An extended property pattern is equivalent to: | |
// | |
// p is { City: { Name: "Antwerp" } } | |
// | |
// which implies `null` checks: | |
// | |
// p != null ... city != null ... | |
// | |
static bool Demo(Person p) => p is { City.Name: "Antwerp" }; | |
} | |
class AsyncMethodBuilders | |
{ | |
// | |
// C# has supported custom async method builders since adding support | |
// for `ValueTask` and `ValueTask<T>`-returning `async` methods. C# 10 | |
// added support to override the builder at the method level. | |
// | |
// We can use this as an opportunity to look behind the scenes of the | |
// async method builder pattern. | |
// | |
class MyTask<T> | |
{ | |
} | |
[AsyncMethodBuilder(typeof(MyTaskBuilder<>))] | |
static async MyTask<int> F() | |
{ | |
await Task.Yield(); | |
return 42; | |
} | |
class MyTaskBuilder<T> | |
{ | |
// | |
// The `F()` method gets compiled into a "ramp" function to construct | |
// an async state machine instance and return a task. The state machine | |
// object implements the logic in the body of the async method. | |
// | |
// Focusing on the ramp function first: | |
// | |
// <F>d__2 stateMachine = default(< F > d__2); | |
// stateMachine.<>t__builder = MyTaskBuilder<int>.Create(); | |
// stateMachine.<>1__state = -1; | |
// stateMachine.<>t__builder.Start(ref stateMachine); | |
// return stateMachine.<>t__builder.Task; | |
// | |
// Therefore, we need to provide the following: | |
// | |
// 1. A static `Create` method. | |
// 2. A `Start` method to kick off the state machine. | |
// 3. A `Task` property to obtain the task to return. | |
// | |
public MyTask<T> Task => default; | |
public static MyTaskBuilder<T> Create() => default; | |
// | |
// Note the use of `ref` to avoid having to box the struct-based | |
// state machine object. The state machine only gets hoisted to the | |
// heap by `SetStateMachine` below in case we hit a suspension, i.e. | |
// we don't run to completion synchronously. | |
// | |
public void Start<TAsyncStateMachine>(ref TAsyncStateMachine stateMachine) | |
where TAsyncStateMachine : IAsyncStateMachine | |
{ | |
} | |
// | |
// See above. This can get called in the async completion path by an | |
// awaiter, see `AwaitOnCompleted`, to hoist the state machine to the heap. | |
// | |
public void SetStateMachine(IAsyncStateMachine stateMachine) | |
{ | |
} | |
// | |
// The body of the `async` method gets rewritten to `MoveNext` method on | |
// the state machine type: | |
// | |
// struct <F>d__2 : IAsyncStateMachine | |
// { | |
// public int <>1__state; | |
// // More fields go here. | |
// | |
// void MoveNext() | |
// { | |
// // State machine code goes here. | |
// } | |
// } | |
// | |
// This `MoveNext` method has a macroscopic structure that looks like: | |
// | |
// try | |
// { | |
// // User code transformed into a state machine. | |
// } | |
// catch (Exception exception) | |
// { | |
// <>1__state = -2; | |
// <>t__builder.SetException(exception); | |
// } | |
// <>1__state = -2; | |
// <>t__builder.SetResult(result); | |
// | |
// where the `-2` state indicates completion, and control flow is massaged | |
// such that all `return` statements end up in the `SetResult` call. So, we | |
// need to provide: | |
// | |
// 1. SetException to capture an unhandled exception in user code | |
// 2. SetResult to store the result of the async method | |
// | |
public void SetException(Exception error) { } | |
public void SetResult(T result) { } | |
// | |
// Finally, all `await` expressions in the body of the `async` method get | |
// transformed into the `GetAwaiter` pattern, e.g. for: | |
// | |
// await Task.Yield() | |
// | |
// we get: | |
// | |
// YieldAwaitable.YieldAwaiter awaiter; | |
// if (num != 0) | |
// { | |
// awaiter = Task.Yield().GetAwaiter(); | |
// if (!awaiter.IsCompleted) | |
// { | |
// num = (<>1__state = 0); | |
// <>u__1 = awaiter; | |
// <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); | |
// return; | |
// } | |
// else | |
// { | |
// awaiter = <>u__1; | |
// <>u__1 = default(YieldAwaitable.YieldAwaiter); | |
// num = (<>1__state = -1); | |
// } | |
// awaiter.GetResult(); | |
// | |
// Note a few things: | |
// | |
// 1. The awaiter is obtained through `GetAwaiter` and is stored in a local. | |
// 2. If the awaiter `IsCompleted` check returns false, the awaiter is lifted | |
// to a field in the state machine object. | |
// 3 .Upon completion, synchronous or otherwise, a call to `GetResult` is made. | |
// | |
// The interesting path is the asynchronous path, where asynchronous completion | |
// is mediated through a call to `Await[Unsafe]OnCompleted` on the builder, | |
// which gets its hand on the awaiter which implements `INotifyCompletion` and | |
// can decide how to trigger the resumption when it's done, e.g.: | |
// | |
// awaiter.OnCompleted(stateMachine.MoveNext) | |
// | |
public void AwaitOnCompleted<TAwaiter, TAsyncStateMachine>(ref TAwaiter awaiter, ref TAsyncStateMachine stateMachine) | |
where TAwaiter : INotifyCompletion | |
where TAsyncStateMachine : IAsyncStateMachine | |
{ | |
} | |
public void AwaitUnsafeOnCompleted<TAwaiter, TAsyncStateMachine>(ref TAwaiter awaiter, ref TAsyncStateMachine stateMachine) | |
where TAwaiter : ICriticalNotifyCompletion | |
where TAsyncStateMachine : IAsyncStateMachine | |
{ | |
} | |
} | |
} | |
} | |
class CSharp11 | |
{ | |
class GenericMath | |
{ | |
// | |
// Generic math is based on static abstract methods. E.g. | |
// | |
// IL_0000: ldarg.0 | |
// IL_0001: ldarg.1 | |
// IL_0002: constrained. !!T | |
// IL_0008: call !2 class [System.Runtime]System.Numerics.IAdditionOperators`3<!!T, !!T, !!T>::op_Addition(!0, !1) | |
// | |
// using `constrained` calls in generic code, which avoids boxing for value types. | |
// | |
static T MidPoint<T>(T first, T second) | |
where T : IAdditionOperators<T, T, T>, IDivisionOperators<T, T, T>, IMultiplicativeIdentity<T, T> | |
=> (first + second) / (T.MultiplicativeIdentity + T.MultiplicativeIdentity); | |
class MyNumber : IAdditionOperators<MyNumber, MyNumber, MyNumber>, IDivisionOperators<MyNumber, MyNumber, MyNumber>, IMultiplicativeIdentity<MyNumber, MyNumber> | |
{ | |
public static MyNumber MultiplicativeIdentity => throw new NotImplementedException(); | |
public static MyNumber operator +(MyNumber left, MyNumber right) | |
{ | |
throw new NotImplementedException(); | |
} | |
public static MyNumber operator /(MyNumber left, MyNumber right) | |
{ | |
throw new NotImplementedException(); | |
} | |
} | |
static int Demo(int x, int y) => MidPoint(x, y); | |
static MyNumber Demo(MyNumber x, MyNumber y) => MidPoint(x, y); | |
} | |
class ListPatterns | |
{ | |
// | |
// List patterns check the expected length based on patterns in element | |
// positions, apply patterns to elements, and can extract `..` in a way | |
// similar to C# 8.0 ranges. | |
// | |
// xs != null && xs.Length == 0 | |
static bool Empty(int[] xs) => xs is []; | |
// xs != null && xs.Count == 1 && xs[0] == 1 | |
static bool Single(List<int> xs) => xs is [1]; | |
// s != null && s.Length >= 1 && s[0] == 'a' | |
static bool Prefix(string s) => s is ['a', ..]; | |
// int length = xs.Length; // Note: struct type, so no null check needed. | |
// return length >= 1 && xs[length - 1] == 1; | |
static bool Suffix(ImmutableArray<int> xs) => xs is [.., 1]; | |
// if (xs != null) | |
// { | |
// int num = xs.Length; | |
// if (num >= 2 && xs[0] == 1) | |
// { | |
// return xs[num - 1] == 9; | |
// } | |
// } | |
static bool PrefixAndSuffix(int[] xs) => xs is [1, .., 9]; | |
// if (xs != null) | |
// { | |
// int num = xs.Length; | |
// if (num >= 2 && xs[0] == 1) | |
// { | |
// // Note the left-to-right evaluation of sub-patterns. | |
// int[] ys = RuntimeHelpers.GetSubArray(xs, new Range(new Index(1), new Index(1, true))); | |
// if (xs[num - 1] == 9) | |
// { | |
// return ys.Length == 7; | |
// } | |
// } | |
// } | |
static bool PrefixAndSuffixSlice(int[] xs) => xs is [1, .. var ys, 9] && ys.Length == 7; | |
// Similar to the above, just using `Slice` instead. | |
static bool PrefixAndSuffixSlice(List<int> xs) => xs is [1, .. var ys, 9] && ys.Count == 7; | |
} | |
class RequiredMembers | |
{ | |
// | |
// How does a `set` become `init`? | |
// | |
// .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) | |
// | |
// How does a property become `required`? | |
// | |
// [RequiredMember] | |
// | |
// How does it get guaranteed? | |
// | |
// [Obsolete("Constructors of types with required members are not supported in this version of your compiler.", true)] | |
// [CompilerFeatureRequired("RequiredMembers")] | |
// public Person() | |
// | |
class Person | |
{ | |
public required string Name { get; init; } | |
public required int Age { get; init; } | |
} | |
} | |
} | |
class CSharp12 | |
{ | |
class LambdaOptionalParameter | |
{ | |
// | |
// The compiler synthesizes custom delegate types, which are generic to | |
// allow for reuse: | |
// | |
// delegate TResult <>f__AnonymousDelegate0<T1, TResult>(T1 arg = 1); | |
// delegate TResult <>f__AnonymousDelegate1<T1, TResult>(params T1[] arg); | |
// | |
// If signatures of type-inferred lambda expressions match Func<> or Action<> | |
// delegate types, those are used. | |
// | |
static void Demo() | |
{ | |
var f = long (int x = 1) => x + 1; | |
var g = (params int[] xs) => xs.Length; | |
} | |
} | |
class CollectionExpressions | |
{ | |
// | |
// Efficient uniform construction of collection type instances. | |
// | |
// Array.Empty<int>() | |
static int[] Empty() => []; | |
// | |
// Uses a `CollectionsMarshal.AsSpan(list)` mechanism to stuff values | |
// in the underlying array without needing repeated bounds checks: | |
// | |
// List<int> list = new List<int>(1); | |
// CollectionsMarshal.SetCount(list, 1); | |
// Span<int> span = CollectionsMarshal.AsSpan(list); | |
// span[0] = 1; | |
// | |
static List<int> One() => [1]; | |
// ImmutableCollectionsMarshal.AsImmutableArray(new int[2] { 1, 2 }) | |
static ImmutableArray<int> Many() => [1, 2]; | |
// Uses `list.AddRange(xs)` to append `xs`. | |
static int[] Spread(IEnumerable<int> xs) => [1, ..xs, 2]; | |
// | |
// Can compute the length of `xs`, so can optimize: | |
// | |
// int num = 0; | |
// int[] array = new int[2 + xs.Count]; | |
// array[num++] = 1; | |
// Span<int> span = CollectionsMarshal.AsSpan(xs); | |
// span.CopyTo(new Span<int>(array).Slice(num2, span.Length)); | |
// num += span.Length; | |
// array[num] = 2; | |
// | |
static int[] Spread(List<int> xs) => [1, ..xs, 2]; | |
} | |
class PrimaryConstructors | |
{ | |
// | |
// The parameters to the primary constructor are in scope of the class | |
// or struct, and can get captured in fields. | |
// | |
class Point(int x, int y) | |
{ | |
// | |
// A read-only property generates: | |
// | |
// private readonly int <X>k__BackingField; | |
// | |
// and | |
// | |
// public int X { get { return <X>k__BackingField; } } | |
// | |
// The right-hand side of the assignment uses `x` and is lifted to | |
// the constructor: | |
// | |
// <X>k__BackingField = x < 0 ? throw new ArgumentOutOfRangeException(nameof(x)) : x; | |
// | |
public int X { get; } = x < 0 ? throw new ArgumentOutOfRangeException(nameof(x)) : x; | |
// | |
// A get-only property with an expression body referencing a primary | |
// constructor parameter causes a closure to be created: | |
// | |
// private int <y>P; | |
// | |
// and the property becomes: | |
// | |
// public int Y { get { return <y>P; } } | |
// | |
public int Y => y; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment