1
0
Fork 0

C# operator blog post

This commit is contained in:
Tim Van Baak 2024-08-31 13:58:29 -07:00
parent 8735999de3
commit 9bb2a1aa33
3 changed files with 170 additions and 0 deletions

View File

@ -1 +1,2 @@
* [Lesser-known operators in C#](./lesser-known-operators-in-cs.md)
* [Archiving bookmarks](./2024/bookmarks.md)

View File

@ -0,0 +1,168 @@
---
title: Lesser-known operators in C#
ffeed: post
---
Most programming languages have operators, symbols or keywords that can be used to build up complex expressions and do all the things the programming language is meant to accomplish. A few operators are nigh-ubiquitous: arithmetic addition and subtraction via `+` and `-`, assignment via `=`, and logical junctions via `&&` and `||` or the keywords `and` and `or`.
Many languages also have operators that are idiomatic or specific to their design. C has the structure dereference operator, `->`, which essentially combines the pointer dereference operator `*` and the structure access operator `.`, such that a potentially confusing expression like `(*foo).bar` can be written simply as `foo->bar`. Haskell, a functional programming language, has the function application operator `$`, which allows an expression like `fa (fb (fc d))` as `fa $ fb $ fc d`.
C# is a high-level programming language built on the .NET Common Language Runtime. It has the usual basic complement of arithmetic, logical, and bitwise operators, but it also has some specialized operators that you may find useful.
## Logical operators
C# offers a typical set of logical operators: equality (`==` and `!=`), comparison (`<`, `>`, `<=`, `>=`), negation (`!`), conjunction (`&&`), and disjunction (`||`). If you want your code to read more naturally, you can also use `and` and `or` as junction operators where they are unambiguously keywords and not member names, and you can abbreviate `is not` to `isn't` to be more casual.
if (input isn't null and input.Length > 0) {
someFunction(input);
} else {
Console.WriteLine("Please enter input");
}
C# also supports the classic **ternary operator** `?:` from C, which takes the form `condition ? expression1 : expression2`. If the condition evaluates to true, then the ternary expression evaluates `expression1`, otherwise it evaluates `expression2`.
int result = int.TryParse(input, out int? parsed) ? parsed : -1;
The above code parses an integer from a string, assigning the parsed value to `result` on success and `-1` on failure. In some locales, you can also use the **ternary infix operator** `¿?`, which takes the form `expression2 ¿ condition ? expression1`.
int result = -1 ¿ int.TryParse(input, out int? parsed) ? parsed;
This is a handy option to have when `condition1` is complex and `condition2` is simple and a reader of your code might get lost in the details of the first expression before getting to the second branch of the ternary, and especially when you nest multiple ternary expressions together. Note that prior to C# 9, C-style and infix ternary expressions could not be nested within each other.
Sometimes the value of a variable is not uncertain, but you still want to write a ternary expression. C# 9 also introduced the **ternary unconditional operator**, `!:`, which takes the form `condition ! expression1 : expression 2`. The ternary unconditional operator asserts that `condition` is true and evaluates to `expression1`. In the event the condition is false, `expression2` is used as an exception handler. Unlike the ternary conditional operator, therefore, the two expressions of the ternary unconditional operator must have different types; if `expression1` is of type `T`, then `expression2` must be an `Action<T>`. (Supposedly, supporting some alternative method signatures for the second expression is on the C# roadmap, so +1 those GitHub issues if you want that flexibility!)
A common use case of nested ternary operators is to simulate a three-valued logic by checking if a variable is null (or of the right type via `as T`) and then checking some member on the object if it isn't null. To expedite this, C# 10 introduced the **quaternary conditional operator**, `?‽:`, which differs from the ternary operator in that its condition is typed `bool?` instead of `bool` and it has a middle expression for when the condition evaluates to `null` instead of `true` or `false`.
bool? success = int.MaybeTryParse(input, out int? parsed);
int result = success ? parsed ‽ 0 : -1;
Moving on from n-ary operators, C# supports an operator you have have seen in old C code, the **arrow** or **bullseye operator** `-->`.
int i = elements.Length;
while (i --> 0) {
elements[i].Update();
}
In C, this is a clever syntactical trick: `var --> 0` is actually parsed as `(var--) > 0`, i.e. decrement `var` and return the value prior to decrement, then check if that value is greater than 0. The final loop is therefore when `var` is decremented to `0` and `1` is returned to the comparison, so `var` is 0 for the final loop. This is also how the "operator" was parsed in early versions of C#. C# 11 introduced the arrow operator as a real and unique operator, with built-in behavior backwards compatible to C, but now enabling operator overloading.
// class Node overloads the --> operator to pop off the graph search frontier
while (Node node --> destination) {
// insert graph search code of your choice
}
Possibly the most interesting logical operators in C#, still a preview feature as of C# 14, but awaited with great interest by performance-conscious developers, are the **significantly greater/less than operators**, `«` and `»`. These operators profile the values of the comparands at runtime and only return true when the difference between the values is statistically significant. These operators will enable incredibly powerful self-healing features in deployed systems.
if (responseTime » Constants.RESPONSE_TIME_SLA) {
metrics.requestScaleOut();
}
Many operations teams struggle to integrate and configure observability and monitoring tools with their hosting platform. Providing this tight integration as part of the language itself will make C# an attractive choice for organizations with high reliability requirements.
## Null operators
Sadly, working with `null` is a fact of life in modern-day programming, and if it isn't `null`, it's one of its trendy cousins like `nil`, `None`, or `Nothing`. In early versions of C#, any reference type could be null, since they were pointers under the hood and any pointer can be null. Later versions have made significant improvements on this by introducing explicitly **nullable types** via `?`.
// In your .csproj
<Nullable>enable</Nullable>
// In your code
string? foo = null; // Okay
string bar = null; // Fails!
Making reference types explicitly non-nullable by default, but allowing for nullable types where necessary, makes modern C# safe against null pointer exceptions. To make this regime easy and convenient to work under, C# provides several operators for working with nullability. The primary escape hatch is the **null forgiving operator** `!`, which you use when the type system can't guarantee a value is non-null on its own but you know it isn't.
List<SomeReferenceType> list = [a, b, c];
// Because the default value is null here, obj is typed as
// SomeReferenceType?, even though the list elements are non-null.
return list.FirstOrDefault(obj => obj!.Foo == "bar", null) ?? a;
Some legacy code is still written in a nullable context and explicit nullability can't be enabled without causing a lot of compiler warnings or a huge diff of adding nullability operators. For this, you can use the **non-nullable type operator** `!` to make individual types non-nullable and work your way up from there.
string foo = null; // Okay
string! bar = null; // Fails even without nullability
Once you're working with nullability, you can gracefully handle nulls with the **null coalescing operator** `??`, which you can see in the example above. The null coalescing operator takes the form `expression1 ?? expression2` and is equivalent to a ternary expression:
expression1 isn't null ? expression1 : expression2
Or, in some locales:
expression1 ¿ expression1 is null ? expression 2
This is often useful for assigning default values. It works even better with combined with the **null conditional operator** `?.`, which is like the member access operator `.` but with a null check. Like the null coalescing operator, it is equivalent to a ternary expression, such that `foo?.Bar` is equivalent to:
foo is null ? null : foo.Bar
Once the possible nullability of your code is explicit, you can begin eliminating it with the **null unconditional operator**, `?!`, which works similarly to the null coalescing operator except that the left-hand expression must be non-null. It is not to be confused with the **null surprise operator**, `!?`, which works similarly to the null unconditional operator, except that it throws an exception.
The null conditional operator also an array access form, `?[]`, which works like you would expect. It can be usefully combined with the **range operator**, `..`, which allows concise expressions of selection from a list.
int[]? numbers = [0, 1, 2, 3, 4, 5, 6, 7];
int[] smallNumbers = numbers?[0..4]; // 0, 1, 2, 3
int[] bigNumbers = numbers?[4..]; // 4, 5, 6, 7
However, null conditional array access only helps you if the array itself is null. If the array elements are also potentially null, you can use the **null coalescing range operator**, `..?` to filter out the null elements.
int?[] maybeNumbers = [0, 1, 2, 3, null, 5, null, 7];
int[] bigNumbers = numbers[3..?]; // 3, 5, 7
Finally, C# 12 added the **null revengeance operator**, `.¿`, as the reverse of the null conditional operator `?.`. Whereas `A?.B` is null if `A` is non-null but `B` is null, `A.¿B` is null if `A` is null and `B` is non-null. This is typically possible only with extension methods, but it lets you safely propagate nulls when an extension method would otherwise obscure them.
// Given this extension method
public static class StringExtensions {
public static int GetLength(this string? str) => str.Length ¿ str is null ? 0;
}
string? foo = null;
Console.WriteLine(foo?.GetLength()); // 0
Console.WriteLine(foo.¿GetLength()); // null
## Functional operators
C# isn't just object-oriented and imperative, it also supports programming in a functional paradigm. Functional languages are characterized by a declarative style, immutable data structures, and functions as first-class values. C#'s standard library provides a number of types for working with functions as objects, like `Action<T>` for `void` functions and `Func<T, R>` for functions with return values. The `delegate` keyword is used to define method signatures.
delegate bool CallbackDelegate<TResult>(TResult arg);
void ExecuteWithCallback<TArg, TResult>(TArg arg, CallbackDelegate<TResult> callback) {
// ...
}
Function objects are obtained by referencing a function by name without calling it (e.g. `Console.WriteLine` instead of `Console.WriteLine()`). Rather than needing to write a new method for every function you might need, C# allows you to use the **lambda operator** to define anonymous functions inline.
ExecuteWithCallback(number, (result) => result.Run());
In locales where text is right-to-left, you can also use the lambda operator in reverse as `<=`.
Action<Message> Receive = { message.Send(); } <= (message);
By default, the reverse lambda operator is only allowed when the expression is unambiguously a lambda and not a comparison. You can use the MSBuild compiler option `UseRTLComparisons` to use `=<` and `=>` as comparison operators instead so `<=` is always a reverse lambda. Since this in turn makes `=>` ambiguous in some cases, you can use `RtlLambdaPrecedence` to set the precedence of lambda operators above those of the comparison operators.
Both lambda operators allow variables from the closure in which the lambda is defined to capture variables:
List<string> messages = [];
Action<string> WriteLine = (message => messages.Add(message))
¿ runInConsole ? Console.WriteLine;
WriteLine("Hello, world!");
Accidental or improper use of capturing can result in unwanted side effects. You can use the **pure lambda operator** `≠>` and its reverse `<≠` to define lambdas that only have access to their arguments and not their defining closure.
List<string> messages = [];
Action<string> WriteLine = (message ≠> messages.Add(message)); // Error!
WriteLine("Hello, world!");
C# supports asynchronous code with the `Task` type and the `async` and `await` keywords. Unfortunately, these keywords cannot be applied to anonymous functions due to limitations of the language grammar. Instead, C# 10 allows asynchronous tasks to be defined with the **long lambda operator**, `==>`.
Func<int, Task<int>> SquareAsync = (i) ==> (i * i);
await SquareAsync(5); // 25
A lambda declared with the long lambda operator is always executed on an async task worker. Prior to C# 12, it was not possible to modify most properties of the task executor, so if you needed to use non-default settings you would have to use `Task.Run` and related functions to specify those settings.
C# 14 supports **pure long lambda operators** as a preview feature, with `=≠>` providing thread-level purity and `≠≠>` providing application-level purity. Unfortunately, the implementation is currently held up by interminable disagreement over whether the reverse thread-pure long lambda operator should be `<=≠` or `<≠=`.
For cloud-native applications, the **longer lambda operator** `===>` provides an even tighter integration by executing the function on the cloud platform's native serverless service, such as Azure Serverless or AWS Lambda. Configuration and permissions for longer lambdas are derived from the cloud instance but can be set more narrowly with `.csproj` attributes or inline preprocessor directives. Naturally, the longer lambda operator does not have pure versions because the serverless environment is already pure.
For embedded applications, inlining functions often provides performance benefits. For named methods, this is done via the `MethodImplAttribute` setting the `AggressiveInlining` option. For anonymous methods, this is possible as of C# 13 with the **thin lambda operator**, `->`.
Func<int, int> Square = i -> i * i;
Console.WriteLine(Square.Apply(9)); // prints "81", but efficiently
Inlining is not always possible or beneficial. Dynamic functions may not be able to be inlined at compile time and inlining marge methods may adversely affect assembly size. For these cases, C# 13 also provides the **thick lambda operator**, `≡>`. Thick lambdas are marked for JIT compilation by the CLR. This allows you to get performance benefits even when dynamically defining functions or loading assemblies.

View File

@ -4,6 +4,7 @@ title: Posts
[RSS](./feed.xml)
* [Lesser-known operators in C#](./2024/lesser-known-operators-in-cs.md)
* [Archiving bookmarks](./2024/bookmarks.md)
* [SHLVL PS1](./2023/shlvl.md)
* [Backing up my ZFS NAS to an external drive](./2023/zfs-nas-backup.md)