There are types and type members.
Class is a reference type.
class YourClassName{}
Preceding the keyword class,
Attributes and Class Modifiers
The non-nested class modifiers are public, internal, abstract, sealed, static, unsafe, and partial.
Following YourClassName,
Generic type parameters and constraints, a base class and interfaces
Within the braces,
Class members - methods, properties, indexers, events, fields, constructors, overloaded operators, nested types, a finalizer
The following sections enumerate each of the class members,
Fields
A field is a variable that is a member of a class or struct
Fields allow the following modifiers - static, public, internal, private, protected, new (inheritance modifier), unsafe, readonly, volatile
popular naming conventions for private fields - firstName or _firstName
readonly prevents a field from being modified after construction. It can be assigned only in its declaration or within the enclosing type’s constructor.
Field initialization is optional. An uninitialized field has a default value (0, ‘\0’, null, false). Field initializers run before constructors. A field initializer can contain expressions and call methods.
You can declare multiple fields of the same type in a comma separated list. Convenient way for all the fields to share the same attributes and field modifiers.
Constants
A constant is evaluated statically at compile time, and the compiler literally substitutes its value whenever used (rather like a macro in C++)
A constant can be bool, char, string, any of the built-in numeric types, or an enum type
Declared with the const keyword and must be initialized with a value
A constant can serve a similar role to a static readonly field, but it is much more restrictive - both in the types you can use and in field initialization semantics.
Another difference is that the evaluation of the constant occurs at compile time. In contrast, a static readonly field’s value can potentially differ each time the program is run. (static readonly DateTime StartupTime = DateTime.Now;)
A static readonly field is also advantageous when exposing to other assemblies a value that might change in a later version. Constants are baked in until you compile again.
Constants can also be declared local to a method
Nonlocal constants allow the following modifiers - public, internal, private, protected, and new (inheritance modifier).
Methods
A method performs an action in a series of statements
It can input data from the caller by specifying parameters
It can output data back to the caller by specifying a return type, and also via ref/out parameters
A method’s signature must be unique within the type. Signature comprises its name and parameter types in order
Methods allow the following modifiers - static, public, internal, private, protected, new, virtual, abstract, override, sealed, partial, unsafe, extern, async
Expression-bodied methods - A fat arrow replaces the braces and return keyword
int Foo (int x) => x * 2;
Expression-bodied functions can also have a void return type
void Foo (int x) => Console.WriteLine (x);
Local methods - You can define a method within another method
Local methods can appear within other function kinds, such as property accessors, constructors, and so on. You can even put local methods inside other local methods, and inside lambda expressions that use a statement block
Local methods cannot be overloaded
Static local methods - Adding the static modifier to a local method (from C# 8) prevents it from seeing the local variables and parameters of the enclosing method
Any methods that you declare in top-level statements are treated as local methods. This means that (unless marked as static) they can access the variables in the top-level statements
A type can overload methods (define multiple methods with the same name) as long as the signatures are different. Whether a parameter is pass-by-value or pass-by-reference is also part of the signature, However, Foo(ref int) and Foo(out int) cannot coexist together.
Instance Constructors
Constructors run initialization code on a class or struct. A constructor is defined like a method, except that the method name and return type are reduced to the name of the enclosing type
Instance constructors allow the following modifiers - public, internal, private, protected, unsafe, extern
Single-statement constructors can also be written as expression-bodied members - public Panda (string n) => name = n;
If a parameter name (or any variable name, for that matter) conflicts with a field name, you can disambiguate by prefixing the field with a this reference - public Panda (string name) => this.name = name;
Overloading constructors - A class or struct may overload constructors. To avoid code duplication, one con structor can call another, using the this keyword public Wine (decimal price) => Price = price; public Wine (decimal price, int year) : this (price) => Year = year;
When one constructor calls another, the called constructor executes first
You can pass an expression into another constructor public Wine (decimal price, DateTime year) : this (price, year.Year) { }
The expression can access static members of the class but not instance members. This is enforced because the object has not been initialized by the constructor at this stage, so any methods that you call on it are likely to fail
Implicit parameterless constructors - For classes, the C# compiler automatically generates a parameterless public constructor if and only if you do not define any constructors. However, as soon as you define at least one constructor, the parameterless constructor is no longer automatically generated
Field initializations occur before the constructor is executed, and in the declaration order of the fields
Nonpublic constructors - Constructors need not be public. A common reason to have a nonpublic construc tor is to control instance creation via a static method call. The static method could be used to return an object from a pool rather than creating a new object, or to return various subclasses based on input arguments
Deconstructors
A deconstructor (also called a deconstructing method) acts as an approximate oppo site to a constructor - whereas a constructor typically takes a set of values (as parameters) and assigns them to fields, a deconstructor does the reverse and assigns fields back to a set of variables
A deconstruction method must be called Deconstruct and must have one or more out parameters
class Rectangle { public readonly float Width, Height; public Rectangle(float width, float height) { Width = width; Height = height; } public void Deconstruct(out float width, out float height) { width = Width; height = Height; }}
The following special syntax calls the deconstructor
var rect = new Rectangle(3, 4);(float width, float height) = rect; // DeconstructionConsole.WriteLine(width + " " + height); // 3 4
The second line is the deconstructing call. It creates two local variables and then calls the Deconstruct method. Our deconstructing call is equivalent to the following
float width, height; rect.Deconstruct (out width, out height);//orrect.Deconstruct (out var width, out var height);
Deconstructing calls allow implicit typing, so we could shorten our call to this
You can offer the caller a range of deconstruction options by overloading the Deconstruct method
The Deconstruct method can be an extension method. This is a useful trick if you want to deconstruct types that you did not author
From C# 10, you can mix and match existing and new variables when deconstructing
double x1 = 0; (x1, double y2) = rect;
Object Initializers
To simplify object initialization, any accessible fields or properties of an object can be set via an object initializer directly after construction
public class Bunny{ public string Name; public bool LikesCarrots, LikesHumans; public Bunny () {} public Bunny (string n) => Name = n;}//Using object initializers, you can instantiate Bunny objects as follows//Note parameterless constructors can omit empty parentheses Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false }; Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };
The code to construct b1 and b2 is precisely equivalent to the following
Bunny temp1 = new Bunny(); // temp1 is a compiler-generated nametemp1.Name = "Bo";temp1.LikesCarrots = true;temp1.LikesHumans = false;Bunny b1 = temp1;Bunny temp2 = new Bunny ("Bo");temp2.LikesCarrots = true;temp2.LikesHumans = false;Bunny b2 = temp2;//The temporary variables are to ensure that if an exception //is thrown during initialization, you can’t end up with //a half-initialized object.
Object Initializers Versus Optional Parameters
Optional parameters have two drawbacks. The first is that while their use in constructors allows for read-only types, they don’t (easily) allow for nondestructive mutation
The second drawback of optional parameters is that when used in public libraries, they hinder backward compatibility. This is because the act of adding an optional parameter at a later date breaks the assembly’s binary compatibility with existing consumers.
The difficulty is that each optional parameter value is baked into the calling site
A final consideration is the effect of constructors on subclassing. Having multiple constructors with long param eter lists makes subclassing cumbersome; therefore, it can help to keep constructors to a minimum in number and complexity and use object initializers to fill in the details
The this reference
The this reference refers to the instance itself
In the following example, the Marry method uses this to set the partner’s mate field
public class Panda{ public Panda Mate; public void Marry (Panda partner) { Mate = partner; partner.Mate = this; }}
The this reference also disambiguates a local variable or parameter from a field
public class Test{ string name; public Test (string name) => this.name = name;}
The this reference is valid only within nonstatic members of a class or struct
Properties
Properties look like fields from the outside, but internally they contain logic, like methods do. A property is declared like a field but with a get/set block added.
get and set denote property accessors. The get accessor runs when the property is read. It must return a value of the property’s type. The set accessor runs when the property is assigned. It has an implicit parameter named value of the property’s type that you typically assign to a private field
public class Stock{ decimal currentPrice; // The private "backing" field public decimal CurrentPrice // The public property { get { return currentPrice; } set { currentPrice = value; } }}
Although properties are accessed in the same way as fields, they differ in that they give the implementer complete control over getting and setting its value. This control enables the implementer to choose whatever internal representation is needed without exposing the internal details to the user of the property. They promote encapsulation.
Properties allow the following modifiers - static, public, internal, private, protected, new, virtual, abstract, override, sealed, unsafe, extern
A property is read-only if it specifies only a get accessor, and it is write-only if it specifies only a set accessor. Write-only properties are rarely used.
A property can also be computed from other data
decimal currentPrice, sharesOwned; public decimal Worth { get { return currentPrice * sharesOwned; } }
Expression-bodied properties
public decimal Worth => currentPrice * sharesOwned; //read-onlypublic decimal Worth { get => currentPrice * sharesOwned; set => sharesOwned = value / currentPrice; }
Automatic properties - The most common implementation for a property is a getter and/or setter that sim ply reads and writes to a private field of the same type as the property. An automatic property declaration instructs the compiler to provide this implementation. The compiler automatically generates a private backing field of a compiler generated name that cannot be referred to. The set accessor can be marked private or protected if you want to expose the property as read-only to other types.
public class Stock { ... public decimal CurrentPrice { get; set; } }
Property initializers
You can add a property initializer to automatic properties, just as with fields - public decimal CurrentPrice { get; set; } = 123;. This gives CurrentPrice an initial value of 123.
Properties with an initializer can be read-only - public int Maximum { get; } = 999;
Just as with read-only fields, read-only automatic properties can also be assigned in the type’s constructor. This is useful in creating immutable (read-only) types
Init-only setters
From C# 9, you can declare a property accessor with init instead of set
public class Note { public int Pitch { get; init; } = 20; public int Duration { get; init; } = 100; }
These init-only properties act like read-only properties, except that they can also be set via an object initializer var note = new Note { Pitch = 50 };
After that, the property cannot be altered note.Pitch = 200; // Error – init-only setter!
Init-only properties cannot even be set from inside their class, except via their property initializer, the constructor, or another init-only accessor
The alternative to init-only properties is to have read-only properties that you populate via a constructor
public class Note { public int Pitch { get; } public int Duration { get; } public Note (int pitch = 20, int duration = 100) { Pitch = pitch; Duration = duration; } }
Should the class be part of a public library, this approach makes versioning difficult, in that adding an optional parameter to the constructor at a later date breaks binary compatibility with consumers (whereas adding a new init-only property breaks nothing)
Init-only properties have another significant advantage, which is that they allow for nondestructive mutation when used in conjunction with records
Just as with ordinary set accessors, init-only accessors can provide an implementation
public class Note { readonly int _pitch; public int Pitch { get => _pitch; init => _pitch = value; } ...
Notice that the _pitch field is read-only - init-only setters are permitted to modify readonly fields in their own class. (Without this feature, _pitch would need to be writable, and the class would fail at being internally immutable.)
Changing a property’s accessor from init to set (or vice versa) is a binary breaking change - anyone that references your assembly will need to recompile their assembly. This should not be an issue when creating wholly immutable types, in that your type will never require properties with a (writable) set accessor.
CLR property implementation
C# property accessors internally compile to methods called get_XXX and set_XXX
public decimal get_CurrentPrice {...} public void set_CurrentPrice (decimal value) {...}
An init accessor is processed like a set accessor, but with an extra flag encoded into the set accessor’s “modreq” metadata
Indexers
Indexers provide a natural syntax for accessing elements in a class or struct that encapsulate a list or dictionary of values. Indexers are similar to properties but are accessed via an index argument rather than a property name.
The string class has an indexer that lets you access each of its char values via an int index
The syntax for using indexers is like that for using arrays, except that the index argument(s) can be of any type(s)
Indexers have the same modifiers as properties and can be called null-conditionally by inserting a question mark before the square bracket
string s = null; Console.WriteLine (s?[0]); // Writes nothing; no error.
Implementing an indexer
To write an indexer, define a property called this, specifying the arguments in square brackets
class Sentence { string[] words = "The quick brown fox".Split(); public string this [int wordNum] // indexer { get { return words [wordNum]; } set { words [wordNum] = value; } } }
Here’s how we could use this indexer
Sentence s = new Sentence(); Console.WriteLine (s[3]); // fox s[3] = "kangaroo"; Console.WriteLine (s[3]); // kangaroo
A type can declare multiple indexers, each with parameters of different types. An indexer can also take more than one parameter
public string this [int arg1, string arg2] { get { ... } set { ... }}
If you omit the set accessor, an indexer becomes read-only, and you can use expression-bodied syntax to shorten its definition
public string this [int wordNum] => words [wordNum];
Indexers internally compile to methods called get_Item and set_Item
public string get_Item (int wordNum) {...} public void set_Item (int wordNum, string value) {...}
Using indices and ranges with indexers
You can support indices and ranges in your own classes by defining an indexer with a parameter type of Index or Range
public string this [Index index] => words [index]; public string[] this [Range range] => words [range];
This then enables the following
Sentence s = new Sentence(); Console.WriteLine (s [^1]); // fox string[] firstTwoWords = s [..2]; // (The, quick)
Primary Constructors (C#12)
From C# 12, you can include a parameter list directly after a class (or struct) declaration
class Person (string firstName, string lastName) { public void Print() => Console.WriteLine (firstName + " " + lastName); }
We can instantiate our class as follows
Person p = new Person ("Alice", "Jones"); p.Print(); // Alice Jones
Primary constructors are useful for prototyping and other simple scenarios. The alternative would be to define fields and write a constructor explicitly
The constructor that C# builds is called primary because any additional construc tors that you choose to (explicitly) write must invoke it. This ensures that primary constructor parameters are always populated.
C# also provides records, records also support primary constructors; however, the compiler takes an extra step with records and generates (by default) a public init-only property for each primary constructor parameter.
Primary constructors displace the default parameterless constructor that C# would otherwise generate.
A primary constructor’s parameters do not disappear out of scope and can be subsequently accessed from anywhere within the class, for the life of the object
(Tbh, not really worth it) It’s mainly for reducing boilerplate
Static Constructors
A static constructor executes once per type rather than once per instance. A type can define only one static constructor, and it must be parameterless and have the same name as the type
class Test { static Test() { Console.WriteLine ("Type Initialized"); } }
The runtime automatically invokes a static constructor just prior to the type being used. Two things trigger this - Instantiating the type and Accessing a static member in the type
The only modifiers allowed by static constructors are unsafe and extern
If a static constructor throws an unhandled exception, that type becomes unusable for the life of the application
From C# 9, you can also define module initializers, which execute once per assembly (when the assembly is first loaded). To define a module initializer, write a static void method and then apply the [ModuleInitializer] attribute to that method
Static field initializers run just before the static constructor is called. If a type has no static constructor, static field initializers will execute just prior to the type being used - or anytime earlier at the whim of the runtime
Static field initializers run in the order in which the fields are declared
Static Classes - A class marked static cannot be instantiated or subclassed, and must be composed solely of static members. The System.Console and System.Math classes are good examples of static classes
Finalizers
Finalizers are class-only methods that execute before the garbage collector reclaims the memory for an unreferenced object. The syntax for a finalizer is the name of the class prefixed with the ~ symbol
class Class1 { ~Class1() { ... } }
This is actually C# syntax for overriding Object’s Finalize method, and the compiler expands it into the following method declaration
You can write single-statement finalizers using expression-bodied syntax
~Class1() => Console.WriteLine ("Finalizing");
Partial Types and Methods
Partial types allow a type definition to be split - typically across multiple files. A common scenario is for a partial class to be autogenerated from some other source (such as a Visual Studio template or designer), and for that class to be augmented with additional hand-authored methods
Each participant must have the partial declaration
Participants cannot have conflicting members. A constructor with the same param eters, for instance, cannot be repeated. Partial types are resolved entirely by the compiler, which means that each participant must be available at compile time and must reside in the same assembly
You can specify a base class on one or more partial class declarations, as long as the base class, if specified, is the same. In addition, each participant can inde pendently specify interfaces to implement
The compiler makes no guarantees with regard to field initialization order between partial type declarations
A partial type can contain partial methods. These let an autogenerated partial type provide customizable hooks for manual authoring
A partial method consists of two parts - a definition and an implementation. The definition is typically written by a code generator, and the implementation is typically manually authored
Partial methods must be void and are implicitly private. They cannot include out parameters
Extended partial methods (from C# 9) are designed for the reverse code generation scenario, where a programmer defines hooks that a code generator implements. An example of where this might occur is with source generators, a Roslyn feature that lets you feed the compiler an assembly that automatically generates portions of your code. A partial method declaration is extended if it begins with an accessibility modifier. Extended partial methods must have implementations. Because they cannot melt away, extended partial methods can return any type and can include out parameters
The nameof operator
The nameof operator returns the name of any symbol (type, member, variable, and so on) as a string
int count = 123; string name = nameof (count); // name is "count"
Its advantage over simply specifying a string is that of static type checking. Tools such as Visual Studio can understand the symbol reference, so if you rename the symbol in question, all of its references will be renamed, too.
To specify the name of a type member such as a field or property, include the type as well. This works with both static and instance members
string name = nameof (StringBuilder.Length);
This evaluates to Length. To return StringBuilder.Length, you would do this
A class can inherit from another class to extend or customize the original class. Inheriting from a class lets you reuse the functionality in that class instead of building it from scratch. A class can inherit from only a single class but can itself be inherited by many classes.
public class Asset { public string Name; } public class Stock : Asset { public long SharesOwned; } public class House : Asset { public decimal Mortgage; } //The derived classes, Stock and House, inherit the `Name` field from the base class, Asset.
Polymorphism - References are polymorphic. This means a variable of type x can refer to an object that subclasses x
Casting and Reference Conversions
An object reference can be
Implicitly upcast to a base class reference
Explicitly downcast to a subclass reference
Upcasting and downcasting between compatible reference types performs reference conversions - a new reference is (logically) created that points to the same object. An upcast always succeeds; a downcast succeeds only if the object is suitably typed
Upcasting
An upcast operation creates a base class reference from a subclass reference
Stock msft = new Stock(); Asset a = msft; // Upcast
After the upcast, variable a still references the same Stock object as variable msft. The object being referenced is not itself altered or converted
Although a and msft refer to the identical object, a has a more restrictive view on that object. Trying to access will give compile-time error
Downcasting
A downcast operation creates a subclass reference from a base class reference
Stock msft = new Stock(); Asset a = msft; // Upcast Stock s = (Stock)a; // Downcast Console.WriteLine (s.SharesOwned); // <No error> Console.WriteLine (s == a); // True Console.WriteLine (s == msft); // True
As with an upcast, only references are affected - not the underlying object. A down cast requires an explicit cast because it can potentially fail at runtime
If a downcast fails, an InvalidCastException is thrown
The as operator
The as operator performs a downcast that evaluates to null (rather than throwing an exception) if the downcast fails
Asset a = new Asset(); Stock s = a as Stock; // s is null; no exception thrown
This is useful when you’re going to subsequently test whether the result is null
if (s != null) Console.WriteLine (s.SharesOwned);
Without such a test, a cast is advantageous, because if it fails, a more helpful exception is thrown
The is operator
The is operator tests whether a variable matches a pattern. C# supports several kinds of patterns, the most important being a type pattern, where a type name follows the is keyword
if (a is Stock) Console.WriteLine (((Stock)a).SharesOwned);
Introducing a pattern variable
You can introduce a variable while using the is operator
if (a is Stock s) Console.WriteLine (s.SharesOwned);
This is equivalent to the following
Stock s; if (a is Stock) { s = (Stock) a; Console.WriteLine (s.SharesOwned); }
The variable that you introduce is available for “immediate” consumption, so the following is legal
if (a is Stock s && s.SharesOwned > 100000) Console.WriteLine ("Wealthy");
And it remains in scope outside the is expression, allowing this
if (a is Stock s && s.SharesOwned > 100000) Console.WriteLine ("Wealthy");else s = new Stock(); // s is in scope Console.WriteLine (s.SharesOwned); // Still in scope
Virtual Function Members
Abstract Classes and Abstract Members
Hiding Inherited Members
Sealing Functions and Classes
The base Keyword
Constructors and Inheritance
Overloading and Resolution
The object Type
Boxing and Unboxing
Static and Runtime Type Checking
The GetType Method and typeof Operator
The ToString Method
Object Member Listing
Structs
Struct Construction Semantics
Read-Only Structs and Functions
Ref Structs
Access Modifiers
To promote encapsulation, a type or type member can limit its accessibility to other types and other assemblies by adding an access modifier to the declaration,
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 (making it less accessible than protected or internal alone)
file (from C# 11)
Accessible only from within the same file. Intended for use by source generators. This modifier can be applied only to type declarations