Code Conciseness and New Features
1. Pattern Matching, Mordern Switch Expression and Expression-Bodied Member
These are powerful syntaxes, they liberate code from cumbersome if-else chains.
Pattern matching:
- type pattern
- property pattern
- list pattern
- { }
Switch expression
- hightly recommended for returning a value based on pattern matching.
- when switch logic effectively functions as a mapping from an input to an output.
- for concise, immutbale assignments.
- leveraging new pattern features like property, positional, relational, and logical patterns
- when compiler exhaustiveness checking is desired to ensure all cases are handled.
The evolution of the switch construct in C# has made it a vastly more powerful and expressive tool for conditional logic, moving it from a simple constant dispatcher to a sophisticated pattern-matching engine.
1 | internal static class ShapeProcessor |
2. Advance Pattern Matching and more Switch Expression
List Pattern
- Matching list of basic types
- Matching list of mixed types
- Matching list of properties
- Matching list of types with properties
- Matching list pattern with logical operator (and, or, not)
- Matching list pattern with relational operator (<, >, <=, >=)
- Matching list of recursive pattern
{}
- check if an object is non-null
- check the values of one or more of an object’s properties
- recursively apply more patterns to those properties (including other property patterns, relational patterns, type patterns, etc.)
1 | // 1. Simple Null Check/type Check(implicit) |
- Various switch expression with pattern matching
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// Property pattern matching
decimal CalculateDeliveryFee(Order order) => order switch
{
{TotalAmount: <50} => 5.00m,
{TotalAmount: >=50} => 0.00m,
- => throw new ArgumentException("Invalid order")
}
// Property pattern matching, or positional pattern, deconstruct an object
string GetPointQuadrant(Point p) => p switch
{
(0, 0) => "Origin",
(int x, int y) when x > 0 && y > 0 => "Quadrant I",
(int x, int y) when x < 0 && y > 0 => "Quadrant II",
(int x, int y) when x < 0 && y < 0 => "Quadrant III",
(int x, int y) when x > 0 && y < 0 => "Quadrant IV",
(_, 0) => "On X-axis", // Any X, Y is 0
(0, _) => "On Y-axis", // X is 0, Any Y
_ => "Unknown"
};
record Point(int X, int Y);
// Relational Patterns
string GetTemperatureCategory(double temp) => temp switch
{
< 0 => "Freezing",
>= 0 and < 10 => "Cold",
>= 10 and < 20 => "Mild",
>= 20 => "Hot"
};
// Logical patterns
3. Primary Constructors
Simplifies constructor declarations by allowing to define parameters directly in the class or struct declaration.
Syntax
- Omit explicit constructor body
- Parameters on Class Signature
- Parameters are in scope for the entire class body
- Initialize properties(or fields) directly with parameters
Compiler behavior under the hood
- Generate an implicit constructor - Primary Constructor
- Compiler implicitly create a private backing field to store the parameter’s value, for property/field/instance method using
- If parameters were only refered by base(), no implicit backing field will be created
Work with normal constructor
- this() has to be called in any other explicitly declared constructors
1 | internal class Vehicle(string make, int year) |
4. Collection Expressions
A concise syntax making code readable for arrays, lists, and spans.
1 | Console.WriteLine("Collection Expressions"); |
5. Nullable Reference Types and “required” Keyword
Nullable Reference Types
- Help to mitigate NullReferenceException errors by allowing to explicitly declare whether a reference type can be null.
- Help to writing more robust and error-free code
- Enabled via
enable in .csproj or #nullable enable in file - It’s about whether a reference can legally be null at any point in its lifetime(not just at initialization)
- This is a compile-time warning system to help prevent NullReferenceException.
- declare a non-nullable reference type, but don’t initialize it to a non-null value by the time the constructor exits.
- assign null to a non-nullable reference type.
- dereference a nullable reference type without first checking for null
- It don’t enforce initialization
required keyword
- enforce that a property or field must be assigned a value, etiher
- in the obejct initializer( new MyClass {Prop = value})
- Call a constructor explicitly marked with attribute [SetRequiredMembers](which tells the compiler that this constructor intends to set all required members, though the compiler doesn’t verify its internal logic)
- This is a compile-time check primarily about initialization, compile-time error will be raised in compile-time if a property didn’t meet above rule.
- enforce that a property or field must be assigned a value, etiher
1 |
|
6. Records
Records are reference types that provide built-in functionality for working with immutable data (data-centric classes). They automatically generate methods like
- Equals()
- GetHashCode()
- ToString()
Good to be used to define data models, especially in functional programming paradigms.
Evolution from class to record:
- A significant shift in how we model data
- Especially when that data is primarily used for holding values and needs well-defined equality (“well-defined equality” refers to a precise and unambiguous definition of when two objects are considered to be the same mathematically, it’s not just about the symbol, but about the underlying properties and rules that govern when that symbol can be used truthfully.).
- A common pain point in OOP: class is a reference, if want to compare the equality of values, we have to manually implement the Equals() and GetHashCode() methods,
- With record, it automates these mundane tasks, providing a dedicated construct for types that embody “value semantics”. It simplifies the creation of immutable data transfer objects (DTOs), domain models, and other data structures.
1 | internal class OldProduct |
7. Default Interface Methods
It allows to add new members to interfaces without breaking existing implementations.
It’s useful for evolving APIs and providing default behavior for interface members.
1 | public interface ILogger |
When good to use DIM:
- API evolution and backward compatibility, it’s also the primary use case
- Provider common helper methods
- Interoperability with other languages/platforms, like Java and Swift with similar features.
When bad to use DIM:
- Don’t use DIM to replace abstract classes
- Don’t use DIM as a magic to hidden behavior. DIM should be simple, predictable and well-documented.
- Don’t use it every where, there will be ambiguity problem
- Don’t use it every where, can violate the interface segregation principle(ISP) from SOLID
- It’s wrong to call DIM directly from implementing calss instance
8. Null-Coalescing Operators
- Null-Coalescing Assignment Operator (??=)
- Assigning a value to a variable only if that variable is currently null
- It’s a shorthand for a common pattern of “initialize if null.”
- Syntax: variable ??= expression;
- if variable is not null, its value remains unchanged, the expression is not evaluated
- if variable is null, variable is assigned the result of expression, the expression is evaluated.
- Equivalent to if(variable == nul){variable = expression;}
1 | string? userName = null; //null |
- Null Coalescing Operator(??)
- hadle null values by providing a default value when an expression evaluates to null
- Syntax: expression1 ?? expression2
- Evaluate expression1(the left-hand operand)
- check for null
- if expression1 is not null, then its value is the result of the entire ?? operation, expression2 is not evaluated.
- if expression1 is null, then expression2(the right-hand operand) is evaluated, and its value is the result of the entire ?? operation.
- Analogy: If the first thing exists(is not null), use it; otherwise, use the second thing.
Its use cases:
- Providing default values for nullable function parameters, or initializing variables with fallback values:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15string? myValue = GetValueFromSomewhere();
string result = myValue ?? myValue : "Defalut Value";
//it is the concise version of
string result = (myValue != null)? myValue:"Default Vaule";
//and
string result;
if(myValue == null)
{
result = "Default Value";
}
else
{
result = myValue;
} - Chaining for multiple fallbacks:
1
2
3
4// Try environment variable, then config file, then hardcoded default
string? environmentVar = Environment.GetEnvironmentVariable("MY_APP_SETTING");
string? configFileSetting = GetConfigFileSetting("MyAppSetting");
string finalSetting = environmentVar ?? configFileSetting ?? "DefaultHardcodedValue"; - Safely accessing properties/methods on nullable value types:
1
2int? age = null; //syntactic sugar for Nullable<int>
int actualAge = age ?? 18;