CSharp 12 In a Nutshell
[!NOTE]
This note is not done
C# is a general-purpose, type-safe, object-oriented, platform-neutral programming language. Its goal is to boost programmer productivity.
- Unified type system, all types share the same base class.
- Class and interfaces
- Properties, methods and events
- functions are values
- events are function members, simplify acting on state changes
- support patterns for purity
- Automatic memory management
- does not eliminate pointers, just make it unnecessary most of the time.
- Supports windows 7+, macOS, Linux, Android & IOS, windows 10 devices.
- Browser through Blazor technology
CLRs, BCLs & Runtimes

-
.NET .NET 8 is Microsoft's flagship open-source runtime, its update history as follows: .NET Core 1.x → NET Core 2.x → .NET Core 3.x → .NET 5 → .NET 6 → .NET 7 → .NET 8. After .NET Core 3, Microsoft removed “Core” from the name and skipped version 4 to avoid confusion with .NET Framework 4.x. .NET framework 4.x is something comes before .NET Core 1.x but it is still supported and in popular use.
-
Windows Desktop and WinUI 3 Both are intended for writing client-rich application that runs on Windows 10 and above. WinUI 3 is a successor of Universal Windows Platform (UWP), and was released in 2022. Windows Desktop APIs existed since 2006 and has huge third party library and community support.
-
MAUI Multi-platform App UI, for developing mobile application on android and iOS, can be used for desktop apps via Mac Catalyst and WinUI 3. It is an evolution of Xamarin.
-
For cross-platform desktop application, a third-party library called Avalonia offers an alternative to MAUI, which is simpler than MAUI with almost complete WPF compatibility.
-
.NET Framework .NET Framework is Microsoft’s original Windows-only runtime for writing web and rich-client applications that run (only) on Windows desktop/server. No major new releases planned, but the latest release will continue to be supported and maintained due to wealth of existing applications.
-
readonly function modifier means it does not modify properties, but not itself cant be modified. It can be used on struct to enforce all fields are readonly.
-
use @prefix
@using
to use reserved word as identifier. -
use
using var reader = File.OpenText(...)
to close file when out of scope -
use
checked
to explicitly check for arithmetic overflow exception (vice versaunchecked
) -
string interpolation
$"number is: {x}"
-
collection expression:
char[] vowels = ['a','e','i','o','u'];
, declare collection with square bracket -
value type like
struct
is initialized with bitwise zero, otherwise it is null -
The design principle here is that users expect the lexical structure of the brackets and commas to be consistent across declaration, initialization and dereferencing.
- about 2d array https://ericlippert.com/2009/08/17/arrays-of-arrays/
-
array bound check is necessary, but optimization can prevent necessary checks, e.g. in a loop and c# provide unsafe code to bypass it.
-
prepend
ref
keyword function parameter to accept value by reference into a function -
string
is value type -
use
out
to return values back from method. -
use
_
when you dont care about a variable- for backward compatibility purpose, it will not work if you have an underscore variable in scope.
-
use
in
like aconst ref
method argument, useful for reducing overhead for copying large value type.- both caller and callee must use the
in
keyword
- both caller and callee must use the
-
last param as varargs
int Sum (params int[] ints)
-
optional params
void Foo (int x = 23) { Console.WriteLine (x); }
-
named arguments
Foo (x:1, y:2); // 1, 2 void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }
- you can mix named and positional arguments
-
When calling ref function, you can call without assigning result to
ref
variable, this fallback to ordinary value:string localX = GetXRef();
-
You can also prevent a ref from being modified:
static ref readonly string Prop => ref x;
-
var
implicit typed local variable -
use
new()
when the class can be inferenced from left hand side -
multi initialize:
a = b = c = d = 0
-
assignment in expression:
y = 5 * (x = 2)
-
binary operators, except for assignment, lambda and null-coalescing operators are left-associative
-
null-coalescing operator:
string s2 = s1 ?? "nothing"
-
null conditional
int x = sb?.ToString().Length; // Illegal: int cant be null] int? x = sb?.ToString().Length; // ok, int? can be null
-
you can open a scope anytime with curly brackets
{}
-
imports
-
using System; System.Console.Writeline("x");
-
using static System.Console; Writeline("x");
-
global using System;
-
-
All type names are converted to fully qualified names
-
you can call
using xxx
within some namespace -
using alias:
using R = System.Reflection; class Program { R.PropertyInfo p; }
-
::
namespace qualification, e.g.global::A.B()
, useful for referring to hidden namespace. -
naming
- private: camel-cased with underscore
_firstName
- local variable: camel-cased
firstName
- public: Cap case:
FirstName
- private: camel-cased with underscore
-
multi-field declare
static readonly int legs = 8, eyes = 2;
-
const
public const string Message = "Hello World"
-
static readonly
maybe different each time the program runs (e.g.DateTime.Now
), butconst
will always be the same (e.g.PI
). -
adding
static
modifier to a local method prevent it from seeing other local variables. -
this
refers to instance itself, invalid when static -
property's
get
andset
method can be overridden- internally, they are compiled to
get_XXX
andset_XXX
- internally, they are compiled to
-
custom indexer
public string this[int wordNum] // indexer { get { return words[wordNum]; } set { words[wordNum] = value; } }
-
static field initializer runs in order they are declared
class Foo { public static int X = Y; // 0 public static int Y = 3; // 3 }
-
finalizer / destructor
~ClassName() { ... }
-
partial methods
-
nameof
operator returns the name of the symbolnameof(count); // count nameof(StringBuilder.Length); // Length
-
use
as
to downcast and evaluate tonull
if fails.Stock s = a as Stock
-
pattern variable
x
:if (x is Stock s) { ... }
-
use
virtual
to declare unimplemented method, andoverride
to implement it in subclass-
you can call parent implementation with
base
keyword -
calling virtual method in constructor is dangerous, because the overriding method may not know it is working on a partially initialized object
-
from c# 9, overriding method can return a subclass type
-
-
abstract
is likevirtual
, but without default implementation -
when you hide a parent class member, use
new
keyword to tell compiler it is intended behaviour to avoid a compiler warningpublic class A { public int Counter = 1; } public class B : A { public new int Counter = 2; }
-
you can also
hide
a method instead ofoverride
it, the difference is, when you call that method using a parent type (which instance is actually a subtype), it will use the parent implementation instead of child implementation, unlikeoverride
, where you get polymorphism. -
subclass must declare its own constructor, but it can call any base class constructor using the
base
keywordpublic class Subclass : Baseclass { public Subclass (int x) : base (x) { ... } }
- if
base
keyword is missing, the base type's parameter-less constructor is implicitly called- if no such constructor exists in base class, subclass must use the
base
keyword
- if no such constructor exists in base class, subclass must use the
- if
-
required
member must be populated via object initializer when constructed -
object
is the base class for all types -
boxing and unboxing
int x = 9; object obj = x; // boxing int y = (int)obj; // unboxing
- array variance only works with reference convertion, not boxing convertion
object[] a1 = new string[3]; // Legal object[] a2 = new int[3]; // Error
- array variance only works with reference convertion, not boxing convertion
-
Point p = new Point(); Console.WriteLine (p.GetType().Name); // Point Console.WriteLine (typeof (Point).Name); // Point Console.WriteLine (p.GetType() == typeof(Point)); // True Console.WriteLine (p.X.GetType().Name); // Int32 Console.WriteLine (p.Y.GetType().FullName); // System.Int32 public class Point { public int X, Y; }
-
ToString()
returns type name if you don't override it -
struct
-
is a value type
-
no inheritance (other than derived from
System.ValueType
) -
no finalizer
-
before c# 10, it is further restricted with no initializer and parameter-less constructors, it is relaxed primarily for benefits of record struct
- even if you define a constructor, a bitwise zero initialization is still possible with
default
keyword:Point p = default
. A good strategy is giving valid value fordefault
state
- even if you define a constructor, a bitwise zero initialization is still possible with
-
ref struct
is a niche feature introduced in c# 7.2
-
-
when you inherit two interfaces with same function, you can implement them by
int I2.Foo()
andint I1.Foo()
, the only way to call it is by casting the instance to the corresponding interface and then call the method((I1)w).Foo();
and((I2)w).Foo();
. -
enums
public enum BorderSide { Left, Right, Top, Bottom } public enum BorderSide : byte { Left=1, Right=2, Top=10, Bottom=11 }
- you can convert it to underlying type by explicit cast
- because enum cast be cast from and to other type, enums' actual value can fall outside of range, e.g.
BorderSide b = (BorderSide)12345; b++; // no error
- you can use
Enum.IsDefined
to check if an enum actually have valid values
- you can use
-
generics
-
you can use value type:
Stack<int>
-
generics does not exists in runtime, only compilation. However, you can have a
Type
that holds unbounded generic typeType a1 = typeof (A<>); // Unbound type (notice no type arguments). Type a2 = typeof (A<,>); // Use commas to indicate multiple type args.
It is used in conjunction with reflection apis
-
use
default
keyword to get the default value for a generic parameter, for reference type, it is null, and bitwise zero for value type. -
you can omit type parameter when the compiler is able to infer it
-
you can specify type constraints
Constraint Description where T : base-class
Base-class constraint where T : interface
Interface constraint where T : class
Reference-type constraint where T : class?
Nullable Reference Types (see Chapter 4) where T : struct
Value-type constraint (excludes Nullable types) where T : unmanaged
Unmanaged constraint. ( T must be a simple value type or a struct that is (recursively) free of any reference types.) where T : new()
Parameterless constructor constraint where U : T
Naked type constraint where T : notnull
Non-nullable value type, or (from C# 8) a non-nullable reference type
-
Runtime type check is possible because every object on heap internally stores a little type token, you can retrieve it by calling GetType
method of object
.
historically speaking, relying on constructors for object initialization could be advantageous in that it allowed us to create fields with read-only access, but this also means we abandoned object initializer (caller side initialization). To solve this problem, c# 9 introduced init
keyword to only allow a property to be set either in constructor or object initializer.
Optional parameters have two drawbacks:
-
It does not easily allow non-destructive mutation (
obj with { ... }
). -
when used in library, it hinders backward compatibility, because adding an optional parameters breaks assembly's binary compatibility with existing consumers
- When application code compiles with library code, library's optional parameter values are copied to the application code, effective making
library.Test()
becomeslibrary.Test(optionalBool=True)
in the application's binary code. If later library decided to changeoptionalBool
to be something else, the application code is not updated automatically. Furthermore, because there is no optional parameter in the binary, if library decided to add a new optional parameters, it breaks application code because now the signature has changed, and application cant find library's new method. It does not just use the default value like python.
- When application code compiles with library code, library's optional parameter values are copied to the application code, effective making
-
Covariance
-
if A convertible to B, T has covariance parameter if T<A> is convertible to T<B>. This means,
- e.g.
IEnumerable<T>
- e.g.
-
typical class cant be covariant,
Stack<Bear> bears = new Stack<Bear>(); Stack<Animal> animals = bears; // Compile-time error animals.Push (new Camel()); // Prevent adding Camel to bears
- For historical reason, array supports covariance, the above code only fails in compile time
-
declare a covariant parameter
public interface IPoppable<out T> { T Pop(); }
- this means
T
can only be at output position, i.e. return type, there is no way to make it as a input parameter and accidentally adding it to the wrong collection - due to limitation in CLR, method parameter with
out
is not eligible for covariance
- this means
-
contravariant is similar concept where you cast upwards, converting from
T<Bear>
toT<Animals>
-
you declare such parameter with
in
keywordpublic interface IPushable<in T> { void Push (T obj); }
- this means you can only use
T
in input position
public interface IComparer<in T> { // Returns a value indicating the relative ordering of a and b int Compare (T a, T b); } var objectComparer = Comparer<object>.Default; // objectComparer implements IComparer<object> IComparer<string> stringComparer = objectComparer; int result = stringComparer.Compare ("Brett", "Jemaine");
- this means you can only use
-
-
c# generics happens in compile time, you write a class, compile it into a
.dll
library, and other application use it freely. Note that this means c# generics must declare all possible values when writing it.-
// OK static T Max <T> (T a, T b) where T : IComparable<T> => a.CompareTo (b) > 0 ? a : b; // Compile error, > operator might not exists in all types static T Max <T> (T a, T b) => (a > b ? a : b); // For C++ template, this is OK template <class T> T Max (T a, T b) { return a > b ? a : b; } // Reason: C++ template exists as source code as part of the application using this code // because this code exists as source code, it is recompiled everytime it is used, // therefore compiler can check on the fly whether the new code's T parameter type // support the > operator and fail if needed.
-
-
Accessibility Level | Description |
---|---|
public | Fully accessible. This is the implicit accessibility for members of an enum or interface. |
internal | Accessible only within the containing assembly or friend assemblies. This is the default accessibility for non-nested types. |
private | Accessible only within the containing type. This is the default accessibility for members of a class or struct. |
protected | Accessible only within the containing type or subclasses. |
protected internal | The union of protected and internal accessibility. A member that is protected internal is accessible in two ways. |
private protected | The intersection of protected and internal accessibility. A member that is private protected is accessible only within the containing type, or from subclasses that reside in the same assembly. |
file (from C# 11) | Accessible only from within the same file. Intended for use by source generators (see "Extended partial methods" on page 125). This modifier can be applied only to type declarations. |
Modifier Type | Modifier |
---|---|
Static modifier | static |
Access modifiers | public internal private protected |
Inheritance modifier | new |
Unsafe code modifier | unsafe |
Read-only modifier | readonly |
Threading modifier | volatile |
Category | Operator symbol | Operator name | Example | User-overloadable |
---|---|---|---|---|
Primary | . | Member access | x.y | No |
Primary | ?. and ?[] | Null-conditional | x?.y or x?[0] | No |
Primary | ! (postfix) | Null-forgiving | x!.y or x![0] | No |
Primary | -> (unsafe) | Pointer to struct | x->y | No |
Primary | () | Function call | x() | No |
Primary | [] | Array/index | a[x] | Via indexer |
Primary | ++ | Post-increment | x++ | Yes |
Primary | -- | Post-decrement | x-- | Yes |
Primary | new | Create instance | new Foo() | No |
Primary | stackalloc | Stack allocation | stackalloc(10) | No |
Primary | typeof | Get type from identifier | typeof(int) | No |
Primary | nameof | Get name of identifier | nameof(x) | No |
Primary | checked | Integral overflow check on | checked(x) | No |
Primary | unchecked | Integral overflow check off | unchecked(x) | No |
Primary | default | Default value | default(char) | No |
Category | Operator symbol | Operator name | Example | User-overloadable |
---|---|---|---|---|
Unary | await | Await | await myTask | No |
Unary | sizeof | Get size of struct | sizeof(int) | No |
Unary | + | Positive value of | +x | Yes |
Unary | - | Negative value of | -x | Yes |
Unary | ! | Not | !x | Yes |
Unary | ~ | Bitwise complement | ~x | Yes |
Unary | ++ | Pre-increment | ++x | Yes |
Unary | -- | Pre-decrement | --x | Yes |
Unary | () | Cast | (int)x | No |
Unary | ^ | Index from end | array[^1] | No |
Unary | * (unsafe) | Value at address | *x | No |
Unary | & (unsafe) | Address of value | &x | No |
Range | .. | Range of indices | x..y | No |
Range | ..^ | x..^y | No | |
Switch & with | switch | Switch expression | num switch { 1 => true, _ => false } | No |
Switch & with | with | With expression | rec with { X = 123 } | No |
Multiplicative | * | Multiply | x * y | Yes |
Multiplicative | / | Divide | x / y | Yes |
Multiplicative | % | Remainder | x % y | Yes |
Additive | + | Add | x + y | Yes |
Additive | - | Subtract | x - y | Yes |
Shift | << | Shift left | x << 1 | Yes |
Shift | >> | Shift right | x >> 1 | Yes |
Shift | >>> | Unsigned shift right | x >>> 1 | Yes |
Relational | < | Less than | x < y | Yes |
Relational | > | Greater than | x > y | Yes |
Relational | <= | Less than or equal to | x <= y | Yes |
Relational | >= | Greater than or equal to | x >= y | Yes |
Relational | is | Type is or is subclass of | x is y | No |
Relational | as | Type conversion | x as y | No |
Category | Operator symbol | Operator name | Example | User-overloadable |
---|---|---|---|---|
Equality | == | Equals | x == y | Yes |
Equality | != | Not equals | x != y | Yes |
Bitwise And | & | And | x & y | Yes |
Bitwise Xor | ^ | Exclusive Or | x ^ y | Yes |
Bitwise Or | Or | `x | ||
Conditional And | && | Conditional And | x && y | Via & |
Conditional Or | Conditional Or | |||
Null coalescing | ?? | Null coalescing | x ?? y | No |
Conditional | ?: | Conditional | isTrue ? thenThis : elseThis | No |
Assignment and lambda | = | Assign | x = y | No |
Assignment and lambda | *= | Multiply self by | x *= 2 | Via * |
Assignment and lambda | /= | Divide self by | x /= 2 | Via / |
Assignment and lambda | %= | Remainder & assign to self | x %= 2 | Via % |
Assignment and lambda | += | Add to self | x += 2 | Via + |
Assignment and lambda | -= | Subtract from self | x -= 2 | Via - |
Assignment and lambda | <<= | Shift self left by | x <<= 2 | Via << |
Assignment and lambda | >>= | Shift self right by | x >>= 2 | Via >> |
Assignment and lambda | >>>= | Unsigned shift self right by | x >>>= 2 | Via >>> |
Assignment and lambda | &= | And self by | x &= 2 | Via & |
Assignment and lambda | ^= | Exclusive-Or self by | x ^= 2 | Via ^ |
Assignment and lambda | = | Or self by | `x | |
Assignment and lambda | ??= | Null-coalescing assignment | x ??= 0 | No |
Assignment and lambda | => | Lambda | x => x + 1 | No |