“Looking at the subset of protests in which demonstrators did not engage in any violence, vandalism, or looting, law enforcement officers were about 3.5 times more likely to use force against leftwing protests than rightwing protests, with about 1.8% of peaceful leftwing protests and only half a percent of peaceful rightwing protests met with teargas, rubber bullets or other force from law enforcement.”

— Lois Beckett in The Guardian

C# Notes

Programming language notes — C# 7.3

C# Notes

These are my C# notes, covering C# 7.3. The notes do not provide an exhaustive description of the language; concepts that seem obvious to me often go unmentioned. Conversely, not everything here is necessarily useful. The notes are not drawn from the C# standard, but from various secondary sources. If you find a mistake, please let me know.

For the most part, code samples follow the Split Notation.

This page includes a two-column print style. For best results, print in landscape, apply narrow margins, change the Scale setting in your browser’s print options to 70%, and enable background graphics. Firefox and Chrome each have their own set of printing bugs, but one of them usually works.

Contents

Value types

Value types can be stored on the stack, though they will be found in the managed heap if they are contained by a class. They consume only the memory used by their data, plus any padding. Value type assignment copies instance content.

All structures are value types, as are bool, char, the predefined numeric types, and enumerations.

Booleans

Type Alias Size
Boolean bool 1 byte

System.Collections.BitArray stores boolean sequences of arbitrary length with just one bit per value.

Integers

Type Alias Size Suffix
Byte byte 1 byte
UInt16 ushort 2 bytes
UInt32 uint 4 bytes U or u
UInt64 ulong 8 bytes UL or ul
SByte sbyte 1 byte
Int16 short 2 bytes
Int32 int 4 bytes
Int64 long 8 bytes L or l

Arithmetic operators are not defined for one or two-byte integers. These operands, whether signed or unsigned, are automatically promoted to Int32.

Integer literals can be prefixed with 0x to produce hexadecimal values:

const UInt16 oLenMax = 0x1000;

Starting with C# 7.0, they can also be prefixed with 0b to produce binary:

byte oMask = 0b_0111_0001;

Digits in any number literal can be grouped arbitrarily with underscores. These are ignored by the compiler.

Overflow checking

Integer expressions evaluated by the compiler are checked for overflow at compile time. Run-time operations are not checked unless marked with the checked operator, which can be applied to expressions:

o = checked (o - 1);

or to blocks:

checked {
  --o;
  ...
}

If overflow occurs in checked code, OverflowException is thrown. Checking can be enabled for the entire program with the /checked+ compiler switch.

Compile-time and run-time checking are both disabled in expressions or blocks marked unchecked:

checked {
  UInt32 o = 0;
  o = unchecked (o - 1);
}

Floating point numbers

Type Alias Size Precision Base Suffix
Single float 4 bytes 7 digits 2 F or f
Double double 8 bytes 15 digits 2 D or d
Decimal decimal 16 bytes 28 digits 10 M or m

Floating point literals can be specified with scientific notation, the exponent being marked with E or e:

const float oTol = 1E-3;

decimal calculations are slower than float or double because it is not a primitive type.

As expected, digits in floating-point literals can be grouped with underscores:

double oWgtMax = 9_999.9;

Special floating point values

float and double instances can be set with the static PositiveInfinity, NegativeInfinity, and NaN constants. Dividing a non-zero number by zero produces PositiveInfinity or NegativeInfinity, depending on the number's sign. Dividing zero by zero, subtracting infinity from infinity, or subtracting negative infinity from negative infinity produces NaN. This value is unequal to every other value, including itself. To check for NaN, call float.IsNaN or double.IsNaN:

float oRate = oDist / oTime;
if (float.IsNaN(oRate)
  || (oRate == float.PositiveInfinity))
  return false;

decimal does not support PositiveInfinity, NegativeInfinity, or NaN. Dividing a decimal value by zero produces a DivideByZeroException.

Characters

Type Alias Size
Char char 2 bytes

Escape sequences represent special characters in character and string literals:

Sequence Character
\0 Null
\a Alert
\b Backspace
\f Form feed
\n Newline
\r Carriage return
\t Tab
\v Vertical tab
\\ Backslash
\' Single quote
\" Double quote

Any Unicode character can be specified with \u and the full four-digit hex value:

const char oCR = '\u000D';

or with \x and the value in four or fewer hex digits:

const char oLF = '\xA';

Enumerations

Enumerations are backed with Int32 by default. Other integer types can be used, but they must be specified with C# type aliases, not full type names:

enum tDir: short {
  N,
  E = 90,
  S = 180,
  W = 270
}

The enumeration name is always specified when referencing enumerations:

tDir oDir = tDir.N;

Unless values are explicitly assigned, the first member is equal to zero, and every succeeding value increases by one. Values can be listed in any order, and they can be repeated without warning from the compiler. Other enumeration members can be referenced when assigning values:

enum tSize {
  Sm,
  Med = Sm,
  Lg = Sm + 10
}

Generally, enumerations must be converted to and from integers with casts. Zero never requires a cast, however. It can be assigned or compared directly:

tStat oStat = 0;

even when the enumeration contains no such value:

enum tStat {
  On = 1,
  Off = 2
}

By extension, if some enumeration instance is a member of a structure, the structure's default constructor will set the instance to zero, even if zero is not a valid value.

Members can be freely combined with bitwise operators, even when the resultant value is not contained by the enumeration:

tOpt oOpt = tOpt.Ver | tOpt.Log;

The Flags attribute is useful for enumerations that are numbered as bitfields. It causes the ToString method to return the names of the 'set' members in some value, rather than the value itself:

[Flags]
enum tOpt {
  None = 0,
  Ver = 1,
  Sync = 2,
  Log = 4
}

The HasFlag method can be used to determine whether a given member is set within a bitfield:

void eApply(tOpt aOpt) {
  if (aOpt.HasFlag(tOpt.Sync)) {
    ...

Boxing

Boxing allocates memory on the managed heap, where reference types are stored, and copies the content of a value type there. It is performed implicitly when a value type:

struct tPt {
  public int X, Y;
}

is assigned to an object reference or an interface:

tPt oPt = new tPt { X = 2, Y = 5 };
object oq = oPt;

A unique allocation is made every time some value type is boxed. Because the value type is copied, changes to the original instance do not affect the boxed data.

Unboxing copies the referenced data back to a value type. Though boxing is implicit, unboxing requires a cast:

tPt oPtLast = (tPt)oq;

When unboxing, the cast must match the instance type exactly, or InvalidCastException will be thrown.

Reference types

The content of a reference type is stored in the managed heap. The reference itself may be stored on the stack, or it may be found in the heap, if it is contained by a class. Reference type instances consume the memory used by their data and padding, plus an additional quantity for overhead, typically twelve bytes. References themselves consume four or eight bytes.

Reference assignment copies the address of an instance, rather than its content. Unlike value types, references can be set to null.

All classes are reference types, as are interfaces, delegates, arrays, and string instances.

Arrays

All arrays derive from System.Array, which implements IList, ICloneable, and IEnumerable<el>.

The length of an array is determined when it is created:

string[] oqDirs = new string[4];

An array reference can be set with a new instance, and the content of an existing instance can be changed, but array instances cannot be resized.

Array elements are allocated contiguously, and value types are stored 'in place', without boxing. Elements can be accessed with the indexer operator, or with the GetValue and SetValue methods. GetValue and SetValue also provide overloads that allow the indices in a multidimensional array to be specified with another array:

void eOut_Lvl(int[] aqIdxs) {
  char[,,] oqLvls = cLvlsCurr();
  Console.Write(oqLvls.GetValue(aqIdxs));
}

Bounds checking is performed automatically, with IndexOutOfRangeException being thrown if the check fails.

Array references and array instances can covary in type:

object[] oqEls = new string[8];

This allows the more specific instance type to be accessed through the more general reference type, which can produce type safety violations at run time. If an attempt is made to assign an invalid type:

oqEls[0] = 0;

ArrayTypeMismatchException will be thrown.

Rectangular arrays

Rectangular arrays are unitary, multidimensional arrays with consistent lengths throughout a given dimension:

char[,] oqCon = new char[80, 25];

The Rank property returns the number of dimensions in the array. Length and LongLength return the number of elements across all dimensions, not the length of a particular dimension. Dimension lengths are retrieved by passing dimension indices to GetLength or GetLongLength:

int oCtX = oqCon.GetLength(0);
int oCtY = oqCon.GetLength(1);
for (Int16 oX = 0; oX < oCtX; ++oX)
  for (Int16 oY = 0; oY < oCtY; ++oY)
    oqCon[oX, oY] = ' ';

Rectangular arrays can be created with initializer expressions, the innermost blocks of which correspond to the rightmost array indices:

Int16[,] oqImg4 = new Int16[,] {
  { 1, 1, 1, 0, 0 },
  { 0, 0, 1, 0, 0 },
  { 1, 1, 1, 1, 1 },
  { 0, 0, 1, 0, 0 }
};

Jagged arrays

Jagged arrays are not unitary; they are arrays of arrays. Component arrays can be one-dimensional or multidimensional, and may vary in size, causing the structure to resemble a tree rather than a matrix.

Subsidiary arrays require separate allocations:

Int16[][][] oqPgs = new Int16[2][][];

oqPgs[0] = new Int16[2][];
oqPgs[1] = new Int16[3][];

oqPgs[0][0] = new Int16[2];
oqPgs[0][1] = new Int16[3];

oqPgs[1][0] = new Int16[2];
oqPgs[1][1] = new Int16[3];
oqPgs[1][2] = new Int16[4];

Jagged arrays can be initialized, but subsidiary arrays must be explicitly allocated:

char[][] oqChs = {
  new char[] { '0' },
  new char[] { '1', '2' },
  new char[] { '3', '4', '5' }
};

Because they are arrays of arrays, the Length method returns the length of the leftmost dimension when applied to jagged arrays.

Creating arrays

Arrays can be created with initializer expressions:

string[] oqDirs = new string[] {
  "N", "E", "S", "W"
};

When this is done, the new expression can be omitted:

string[] oqDirs = { "N", "E", "S", "W" };

If the array length is not specified, it is inferred from the initializer; if it is specified, it must match the length of the initializer. If no initializer is provided, the elements are default-initialized.

It is also possible to omit the type from the new expression, allowing the compiler to infer the element type:

string[] oqDirs = new[] { "N", "E", "S", "W" };

This is necessary when creating an array of anonymous types:

var oqMarks = new[] {
  new {
    ID = 85,
    Pos = new tPt(12, 20),
    Wt = 60.0F
  },
  new {
    ID = 90,
    Pos = new tPt(15, 44),
    Wt = 64.0F
  }
};

Arrays can also be created with the static CreateInstance methods. One of the CreateInstance overloads allows arrays to be created with index bases other than zero, as long as the bases are positive:

string[,] oqIDs;
if (oCk) {
  int[] oqLens = { 12, 24 };
  int[] oqIdxsMin = { 256, 512 };
  oqIDs = Array.CreateInstance(
    typeof(string),
    oqLens,
    oqIdxsMin
  ) as string[,];
  oqIDs[256, 512] = "ID63";
  ...

The lower and upper indices can be retrieved with GetLowerBound and GetUpperBound.

CreateInstance returns instances of type System.Array. As shown, these can be cast to specific array types with as.

The static Resize method accepts a ref to an array reference. After copying the array to a new instance with a different size, it assigns the new array to the original reference.

Copying arrays

Entire array instances can be copied with Clone.

The static Copy methods copy ranges between one array and another. The non-static CopyTo methods copy ranges as well, but their lengths cannot be specified. The static ConstrainedCopy method copies a range and undoes the copy if it cannot be completed.

The static ConvertAll method accepts a Converter<in, out> delegate that it uses to convert one array into another with a different element type.

The static AsReadOnly method returns the array within a ReadOnlyCollection<el> wrapper.

Reading arrays

The static Exists method returns true if any element matches a supplied Predicate<el>. The static Find and FindLast methods return the first or last element to match the predicate, and FindAll returns all elements that match. The static TrueForAll method returns true if the predicate is true for all elements.

The IndexOf and LastIndexOf methods return the index of the first or last element that matches the specified value, after scanning the entire array, or a range thereof. The static FindIndex and FindLastIndex methods return the index of the first or last element that matches the supplied Predicate<el>.

The BinarySearch methods return the index of the first element that matches the supplied value within a sorted one-dimensional array, or within a range thereof. If the value is not found, the bitwise complement of the index that would be occupied is returned. Elements can be compared with their own IComparable or IComparable<el> implementations, or with a supplied IComparer or IComparer<el> implementation.

Modifying arrays

Unlike List.Clear, the static Clear method does not remove elements. It sets a range of elements to their default values.

The static Reverse methods reverse elements in place within the array, or within a range.

The static Sort methods sort the array in place. Some overloads accept keys and items arrays, both of which are sorted according to the values in keys, as though the arrays were a single Dictionary<key, item>. Other overloads sort a range within the array. Elements can be compared with their own IComparable or IComparable<el> implementations, with a supplied IComparer or IComparer<el> implementation, or with a Comparison<el> delegate.

string

String data is usually represented with System.String, which is aliased as string. Though string is a reference type, its equality and inequality operators are overloaded to compare instance content, not addresses.

The string concatenation operator automatically converts non-string operands with ToString:

string oqText = 9 + " coins";

Instances are immutable, so complex string concatenations are performed more efficiently with System.Text.StringBuilder.

The static string property Empty represents an empty string. The static method IsNullOrEmpty returns true if the specified reference is null, or if it references an empty string.

The Length property returns the string length.

The static Compare method compares strings or substrings, with options for case-insensitive or culture-specific comparisons, as well as substring comparisons. The non-static CompareTo method also compares strings, but with fewer options.

The StartsWith, Contains, and EndsWith methods detect substrings within an instance. The IndexOf and LastIndexOf methods locate characters or substrings. The IndexOfAny and LastIndexOfAny methods locate one of a set of characters.

The static Concat methods concatenate multiple strings or object string representations, these passed as distinct parameters, or string or object arrays. The static Join methods concatenate the elements of a string array, delimiting each with a substring.

The Insert method returns the value of an instance with a substring inserted at a particular position. The Remove method returns the value with a substring removed. The Replace method returns the value with all occurances of one character or substring replaced by another.

The PadLeft and PadRight methods return the value padded with spaces or a specified character. The Trim, TrimStart, and TrimEnd methods return the value trimmed either of whitespace or of all elements in a character array.

The Substring method returns a substring from the value. The Split method returns a string array containing all substrings delimited in the value by a specified character or substring.

The ToUpper and ToLower methods return the value shifted in case.

The static Format methods substitute values into format strings:

String oqLbl = "Rate";
float oVal = 12.1F;
string oq = string.Format("{0}: {1}", oqLbl,
  oVal);

The ToCharArray methods return character arrays representing the string or a substring.

string implements:

  • IEnumerable<char>
  • IComparable<string>
  • IEquatable<string>
  • IConvertible
  • ICloneable

Verbatim strings

Escape sequence processing can be disabled by prefixing string literals with @:

string oqPath = @"C:\Temp\";

Verbatim strings capture all whitespace between their quotes, including tabs, carriage returns, and linefeeds:

string oqHead = @"Line 1
Line 2
Line 3";

Double quotes are specified within such strings by escaping each with another double quote:

string oqText = @"The goblin says ""Spare a coin?""";

Interpolated strings

Prefixing a string literal with $ creates an interpolated string, which can contain one or more C# expressions within curly braces:

int oCt = cRead_Int();
string oText = $"Max value: {(1 << oCt) - 1}";

The expressions are verified at compile time, and their run-time results are automatically converted to strings. Expression output can be formatted with format strings, much the way substitutions are formatted in String.Format:

cLog($"Batch {oNum:D3}: Code {oCd:X}...");

To include a curly brace in the string, repeat the character, whether it is an opening brace or a closing one. Interpolated strings cannot exceed more than a single line unless they are also defined as verbatim strings; when this is done, the $ prefix must precede the @.

Tuples

In C#, tuples can be defined with the Tuple classes, or with the newer ValueTuple structure.

Tuple class

Tuples were originally represented with the generic Tuple classes, which can be specialized to store up to seven values. Instances can be constructed directly:

var oDistTag = new Tuple<float, string>(1.0F, "BASE");

or by invoking the static Tuple.Create method, which infers the types:

var oDistTag = Tuple.Create(1.0F, "BASE");

Tuple values are stored in read-only properties named Item1, Item2, et cetera. Note that the numbers are one-indexed:

float oDist = oDistTag.Item1;

An additional specialization accepts an eighth type that can be used to store more values.

ValueTuple structure

C# 7.0 introduces the generic ValueTuple structure, with special language support for creating tuples and reading their data. Because it is a value type, ValueTuple also offers better performance in some cases.

A tuple can be created by enclosing two or more values in parentheses. When initialized with literals, the tuple fields are named Item1, Item2, et cetera. As before, these numbers are one-indexed:

var oDistTag = (1.0F, "BASE");

When initialized with variables, the tuple fields borrow the variable names:

Dist = 1.0F;
Tag = "BASE";
var oDistTag = (Dist, Tag);

Fields can be explicitly named by prefixing them with a name, followed by a colon:

var oDistTag = (Dist: 1.0F, Tag: "BASE");

Names like Item1 cannot be explicitly assigned, nor can the same name by assigned more than once.

Tuples can be nested within other tuples:

var oData = (101, "XA", (1.0, 0.0));

Unlike their Tuple class counterparts, fields in ValueTuple are mutable.

In C# 7.3, tuples implement equality and inequality operators that perform memberwise comparisons with implicit type conversion, plus operator lifting for nullable values. Names are ignored when comparing fields, but a compiler warning results if one field is explicitly named, and if its counterpart bears a different name.

Tuples can be assigned to other tuple instances if they contain the same number of fields. Values are implicitly converted, and field names are ignored. No field names are changed by the assignment.

Tuples can be returned from functions. When this is done, the tuple field types must be specified in the return signature, but the fields can be named or unnamed. When they are named, the signature resembles the tuple deconstruction syntax, but is unrelated:

static (float Dist, string Tag) cDistTag() {
  float oDist;
  string oTag;
  ...
  return (oDist, oTag);
}

Tuples can be stored in containers, which is useful when returning results from LINQ:

static IEnumerable<(float Dist, string Tag)> cMarksAct() {
  return
    from oMark in cMarks
    where (oMark.Dist > cDistMin)
    select (oMark.Dist, oMark.Tag);
}

Deconstructing tuples

A group of variables can be assigned with tuple values in a single statement, simply by enclosing them within parentheses, and assigning with the tuple instance. This is called tuple deconstruction:

float oDist;
string oTag;
(oDist, oTag) = cMarkActFirst();

These can be local variables, or members of the containing class or structure.

New destination variables can also be defined within the deconstruction expression:

(float oDist, string oTag) = cMarkActFirst();

When this is done, specific field types can be inferred with var:

(var oDist, string oTag) = cMarkActFirst();

All field types can be inferred by moving var outside the expression:

var (oDist, oTag) = cMarkActFirst();

It is not possible to define new variables and assign to existing variables within the same expression, however.

If one or more fields are not needed, their places can be filled with underscore characters, which are called discards:

(oDist, _) = cMarkActFirst();

These values are ignored by the deconstruction operation.

Deconstructing other types

A non-tuple type can be made deconstructible by defining a Deconstruct method that accepts destination variables as out parameters. The method must be public and it must return void:

public struct tPt {
  public tPt(float aX, float aY) { X = aX; Y = aY; }
  public float X, Y;

  public void Deconstruct(out float aX, out float aY) {
    aX = X;
    aY = Y;
  }
}

Deconstruct can also be defined as an extension method, allowing deconstruction of second-party types:

public static class tUtil {
  public static void Deconstruct(this tPt aPt,
    out float aX, out float aY, out double aDist) {

    aX = aPt.X;
    aY = aPt.Y;
    aDist = Math.Sqrt((aX * aX) + (aY * aY));
  }
}

The same type can bear multiple Deconstruct methods, as long as they write to different numbers of variables.

Classes and structures

Classes are reference types, while structures are value types. More particularly, structures differ from classes in that:

  • They are passed by value, unless marked with ref or out;
  • Multiple instances are allocated in a single block of memory, when stored within an array;
  • They can be allocated on the stack, though they will be found in the managed heap if they are contained within a class;
  • They do not support explicit inheritance, though they inherit implicitly from ValueType and object;
  • They always provide implicit default constructors, preventing parameterless constructors from being explicitly defined. By extension, the default value of every structure is determined by the default values of its members;
  • They require that every variable be initialized in every constructor;
  • They cannot use variable or property initializers, which assign values in the member definitions;
  • They cannot define virtual methods or finalizers.

Because they are passed by value, and because they promote locality of memory when stored in arrays, structures are used to store small packets of data. To prevent unnecessary copying, larger amounts should be stored in classes.

Starting with C# 7.2, structures can be declared readonly:

public readonly struct tZone {
  public tZone(string aCd, int aCtMax) { Cd = aCd; CtMax = aCtMax; }
  public string Cd { get; }
  public int CtMax { get; }
}

Marking a structure this way ensures that it is immutable. All non-static variables must be readonly, and no property can define a setter, or a compiler error will result.

Structures can also be declared ref. These structures can only be allocated on the stack, so they cannot be embedded within classes, which are stored on the heap. They also cannot be boxed, nor can they implement interfaces, and they cannot be used as local variables within async methods, iterators, lambda expression, or local functions.

static

Static constructors are executed once per class or structure type, before any other static method in that type, before the first instance of the type is created. A class or structure can define only one static constructor, and it can have no access modifier or parameters:

struct tRoll {
  static tRoll() {
    ...

If the static constructor throws an exception, any future attempts to create instances of that type will cause TypeInitializationException to be thrown.

Entire classes can be declared static if all their members are static:

static class tqMetr {
  public static float sHgtMax;
  ...

Though they can define static members, structures cannot be static.

using static

Just as ordinary using directives allow namespace members to be referenced without being qualified by the namespace name, using static allows the static members of a type to be referenced without qualifying them with the type name:

namespace nMain {
  using static tqCache;

  class tqCache {
    public static void sPrep() {
      ...

  class Program {
    static void Main(string[] aqArgs) {
      sPrep();
      ...

The directive must be placed within the using namespace, not above, where other using directives would be found.

Constructors

If no constructor is explicitly defined for some class, the compiler will create a default constructor. Structures are always provided with default constructors, so parameterless constructors cannot be explicitly defined for them. Structure constructors must assign every variable in the structure.

A constructor can invoke another constructor in the same class or structure with this:

class tqMgr {
  public tqMgr(int aCt) { Ct = aCt; }

  public tqMgr(int aCt, int aCtMax): this(aCt) {
    CtMax = aCtMax;
  }
  ...

A constructor in the base class is invoked with base:

class tqMgrStd: tqMgr {
  public tqMgrStd(int aCt): base(aCt) {}
  ...

Starting with C# 7.0, constructors can also be expression-bodied.

Finalizers

A class's finalizer is executed just before the garbage collecter deallocates an instance of the class. Defining a finalizer is equivalent to overriding object.Finalize, which cannot be overridden any other way:

class tqTree {
  ~tqTree() {
    ...
  }
  ...

Child class finalizers are invoked before those of parent classes. Starting with C# 7.0, finalizers can also be expression-bodied.

Structures cannot define finalizers. Because they are triggered by garbage collection, the exact timing of finalization operations cannot be predicted.

Properties

Properties must define a get accessor, a set accessor, or both. Within a set accessor, the incoming operand is represented by value:

int eWth;
public int Wth {
  get { return eWth; }
  set { Right = value - Left; }
}

Automatic properties do not require the definition of a backing variable. They can be assigned in-place with an initializer:

public int Ttl { get; set; } = 8;

Properties cannot be declared readonly or const. They are made read-only or write-only by omitting the relevant accessor, or by changing its access level. When the set accessor is omitted, the resulting read-only property cannot be assigned except with an initializer, or in the constructor, just as if it were a readonly class variable:

class tqHold {
  public tqHold(int aLen) {
    Len = aLen;
  }

  public int Len { get; }
  ...

If an access modifier is applied to one of the accessors, it must be less accessible than the property as a whole:

public int Ct { get; protected set; }

For convenience, read-only properties can be expression-bodied. Instead of defining a get accessor, these join an expression with the property name using something like the lambda expression syntax:

public int CtNext => cCt + cLenPad;

Starting with C# 7.0, property accessors can also be expression-bodied:

public int CtNext {
  get => cCt + cLenPad;
  set => cCt = value - cLenPad;
}

Indexers

Indexers are implemented much like properties, but instead of being given a name, they are defined with this. They accept one or more parameters of any type, within square braces:

class tqMgr {
  public int this[string aqKey, char aCd] {
    get { return tqStore.sRest(aqKey, aCd); }
    set { tqStore.sSave(aqKey, aCd, value); }
  }
}

Indexers can be overloaded, just like other methods. They are dereferenced like an array:

tqMgr oqMgr = new tqMgr();
oqMgr["alpha", 'a'] = 100;

They can also be expression-bodied, like properties:

public int this[int aIdx] => cEl("BACK", aIdx);

Methods

Every function must be contained by a class, or (starting with C# 7.0) another function. Local function definitions can be placed anywhere in the containing function, even below the function's invocation:

int eIdxTop() {
  ...
  return oIdx(oCd);

  int oIdx(string aCd) {
    ...
  }
}

Methods can be expression-bodied, even if they return void:

class tMgr {
  void Que(int aIdx) => cQue(cEl("OUT", aIdx));
  ...

Parameters

ref

Parameters declared with ref are passed by reference:

public static void sSwap(ref int ar0, ref int ar1) {
  int o0 = ar0;
  ar0 = ar1;
  ar1 = o0;
}

Such a parameter aliases its argument rather than copying it. This allows the value outside the method to be modified from within. Note that passing a reference is not the same as passing by reference.

If parameters are marked ref in the declaration, those arguments must be marked ref when the method is invoked:

sSwap(ref oX, ref oY);

Variables must be assigned before being passed as ref arguments.

Starting with C# 7.0, methods can also return ref values:

ref int cIdxNext() {
  if (cCkBack) return ref cIdxBack;
  return ref cIdxBase;
}

Along with the return signature, all return statements must be marked ref. Reference-returned variables must live longer than the function, and temporary objects in particular cannot by returned this way. Neither can ref values be returned from async methods.

Local variables can be declared ref as well, and these will alias the variables produced by functions that return ref:

ref int orIdx = ref cIdxNext();
orIdx = 0;

When assigning a function result to a local ref, both the source and destination must be marked ref.

Omitting ref from the source and destination causes the result to be returned by value:

int oIdx = cIdxNext();

Starting with C# 7.2, functions can return ref readonly values. These are also returned by reference, but the data they reference cannot be modified:

ref readonly List<tPt> cPtsReady() {
  ...

If such a function is assigned to a local ref variable, that variable must be ref readonly as well:

ref readonly List<tPt> oPts = ref cPtsReady();
in

Starting with C# 7.2, parameters can also be marked in. These are also passed by reference, but they cannot be modified within the function:

void cAdd(in tPt aPt) {
  ...

Such arguments can be marked in, but they do not need to be.

out

Parameters declared with out are also passed by reference, but they are meant to accept return values, and their arguments need not be assigned before being passed. out arguments must be prefixed with out:

int oTop;
int oHgt = 10;
eReset(out oTop, out oHgt);

out parameters must be assigned within the function before the function ends. They also must be assigned within the function before being used in the function, even if they were assigned before being passed:

void eReset(out int arTop, out int arHgt) {
  ...
  arTop = 0;
  arHgt = 0;
}

Starting with C# 7.0, out argument variables can be declared within the argument list, simply by including their types:

eReset(int out oTop, int out oHgt);

If a value is not needed, its argument can be replaced with a discard:

int oTop;
eReset(int out oTop, out _);

Note that the out keyword is still required.

params

If a function's last parameter is an array, and if it is declared params:

void eOut(string aqName, params int[] aqVals) {
  ...

then any number of arguments of the array type can be passed in its place:

eOut("Rates", 2, 4, 8);

An array of the same type can also be passed through the params parameter:

int[] oqRates = { 2, 4, 8 };
eOut("Rates", oqRates);
Optional parameters

The last entries in a parameter list can provide default values, producing one or more optional parameters:

void eMark(int aIdx, int aCt = 0, string aFld = "DEF") {
  ...

Every default must be a constant expression, or the default value of a structure:

float eSumm(tPt aPos = new tPt()) {
  ...

Default values can also be specified with the default operator:

float eSumm(tPt aPos = default(tPt)) {
  ...

Non-default constructors cannot be used to specify default values, and class instances can never be used as defaults. Neither can optional parameters be marked ref or out.

Optional parameters can be marked with Caller Info attributes that cause the specified defaults to be replaced at run time. These can be used to obtain the name of the calling method or property, or the file name and line number from which the call was made:

public static void Log(
  [CallerMemberName] string aqNameFunc = null,
  [CallerFilePath] string aqPathNameFile = null,
  [CallerLineNumber] int aLine = 0
) {
  ...
Named arguments

Ordinarily, function arguments must be provided in the same order in which the parameters are declared. If the arguments are prefixed with parameter names, however, they can be passed in any order, so long as all unnamed arguments are specified first:

void eAdd(int aPos, string aqName, char aCd = '0') {
  ...

void eExec() {
  eAdd(0, aCd: 'A', aqName: "Begin");
  ...

If a function has many default parameters, this allows a few custom values to be specified while the other defaults are retained.

Overloads

Overload calls are resolved at compile time, so static typing is used when matching arguments with overload signatures. Preference is given to the most-derived class in a hierarchy:

class tqIt { ... }
class tqItKey: tqIt { ... }

struct tInv {
  public void Add(tqIt aqIt) { ... }
  public void Add(tqItKey aqIt) { ... }
}

Parameters passed by value are distinct in signature from those passed by reference:

class tqLoop {
  public void Exec(int aCt) { ... }
  public void Exec(ref int arCt) { ... }

  public void Ck(int aIdx) { ... }
  public void Ck(out int arIdx) { ... }
}

ref parameters are not distinguished from out parameters, however.

Operator overloads

All operator overloads are static, so operands are always represented by parameters, and never by this. Overloads must be declared public, and at least one parameter type must match the containing type.

The operand order is considered when resolving overload calls, so binary operators accepting mixed types may require two implementations:

struct tPt {
  public static tPt operator*(tPt aPt, int a) {
    return new tPt(aPt.X * a, aPt.Y * a);
  }

  public static tPt operator*(int a, tPt aPt) {
    return aPt * a;
  }
  ...

If an arithmetic operator is overloaded, the corresponding compound assignment operator is overloaded automatically:

tPt oPt = new tPt(1, 2);
oPt *= 10;

The short-circuiting logical operators cannot be explicitly overloaded. They are implicitly overloaded when the corresponding bitwise logical operators are overloaded.

If one comparison or equality operator is overloaded, its complement must be overloaded as well. Overloading the equality operators generally entails that object.Equals and object.GetHashCode be overridden. Overloading the comparison operators often entails that IComparable or IComparable<el> be implemented.

Extension methods

An extension method is a static method that can be applied to an instance of an unrelated type as though it were defined by that type. It is defined within a non-generic static class, and its first parameter, identifying the type to which it can be applied, is declared with this:

static class tqExtPt {
  public static bool sNear(this tPt aPtThis, tPt aPt) {
    int o = Math.Abs(aPtThis.X - aPt.X);
    if (o > 1) return false;

    o = Math.Abs(aPtThis.Y - aPt.Y);
    return (o <= 1);
  }
  ...

When the method is applied to an instance, that instance is passed as the first parameter:

tPt oPt0 = new tPt(0, 0);
tPt oPt1 = new tPt(1, -1);
bool oNear = oPt0.sNear(oPt1);

Extension methods can also be called as normal static methods:

oNear = tqExtPt.sNear(oPt0, oPt1);

Though they are used like members of the instance type, extension methods gain no special privileges with respect to that type. In particular, they cannot access its non-public members.

Extension methods can target interfaces just as they do classes and structures:

interface fMsg {
  string Text();
}

class tqMsg: fMsg {
  public string Text() {
    ...

static class tqExtMsg {
  public static void sOut(this fMsg aqMsg) {
    Console.WriteLine(aqMsg.Text());
  }
}

allowing them to be invoked through the interface as though they were members of the interface:

fMsg oqMsg = new tqMsg();
oqMsg.sOut();

If the name of an extension method conflicts with that of a method defined in the instance type, the instance method is given precedence. If two extension methods conflict, precedence is given to the one that more closely matches the calling signature. In both cases, if another resolution is desired, the method can be called in its static form.

extern

Methods declared extern are implemented externally, in a language other than C#. Such declarations can be used to call functions imported from unmanaged libraries:

[DllImport("Port.dll")]
public static extern byte Read(Int32 aIdx);

Access levels

Subclasses can be made less accessible than parent classes, but not more accessible. Access levels cannot be changed when overriding methods.

public

public members are accessible anywhere. This is the default level for enumeration and interface members.

internal

internal members are accessible within the local assembly, and within friends of the local assembly. An assembly is declared as a friend by adding the InternalsVisibleTo attribute to any unit:

[assembly: InternalsVisibleTo("AssemAdd")]

This is the default level for non-nested types.

protected

protected members are accessible throughout the containing class or structure, and throughout its subclasses, even those in different assemblies. This extends to any types that are nested within the containing type or its subclasses. C# does not support friend classes, but nested types can be used to similar effect.

protected internal

protected internal members are accessible everywhere protected or internal members are accessible.

private

private members are accessible throughout the containing class or structure, and throughout any types nested therein. This is the default level for class and structure members.

private protected

Starting with C# 7.2, members can be declared private protected. These are accessible throughout the containing class or structure, and throughout its subclasses, so long as those subclasses are part of the same assembly.

Partial types

Classes, structures, and interfaces declared partial can be defined in multiple blocks, even blocks in different files:

partial struct tLvl {
  string Name;
}
...

partial struct tLvl {
  tPt Size;
}

When this is done, all blocks must be declared partial, all must have the same access level, and all must be part of the same assembly. Declaring any block abstract or sealed declares the type as a whole this way. A base class can be specified by one or more blocks, so long as only one such class is identified. If different base interfaces or attributes are specified by different blocks, the type as a whole implements all of them.

Partial methods

Partial classes and structures can declare partial methods, which can be defined in one block and implemented in another:

partial class tqMgr {
  partial void cExec(string aqText);
}

partial class tqMgr {
  partial void cExec(string aqText) {
    ...
  }
}

Both the definition and the implementation must be marked partial.

Partial methods must have void return type, and they cannot be marked virtual, abstract, new, override, sealed, or extern. They can include ref parameters, but not out parameters. They cannot bear access modifiers, so they are always private.

Partial methods that are defined but not implemented are removed from the class, and calls to them are discarded.

Anonymous types

Anonymous types group fields into a class without explicitly defining that class. They are created and initialized simultaneously:

float oWt = 18.0F;
var oqMark = new {
  ID = 100,
  Pos = new tPt(10, 25),
  Wt = oWt
};

The names and types in the initializer are used to define a set of read-only properties in the unnamed class:

tPt oPos = oqMark.Pos;

If a variable is provided as an initializer, but no name is specified:

Int16 Curr = 12;
Int16 Next = 0;
var qLink = new { Curr, Next };

that variable's name is applied to the property:

Console.WriteLine(qLink.Next);

Only classes can be created this way. Anonymous types directly subclass object, and cannot be cast to any other type. Types that are defined with the same members in the same order are treated as the same type. They have method scope, so they cannot be passed from a method without first being cast to object.

Instance equality

Instance equality can be tested with methods defined in object, or with the equality operator. By default, different notions of equality apply to reference types and value types.

ReferenceEquals

object.ReferenceEquals compares instance addresses, as when references are passed to the default equality operator:

public static bool ReferenceEquals(object,
  object);

It is not appropriate for use with value types. Passing these to ReferenceEquals causes them to be boxed, and the comparison therefore fails, even when an instance is compared with itself.

static Equals

After verifying that the references are not identical or null, the static object.Equals method returns the output of the non-static Equals method, as invoked from the first instance:

public static bool Equals(object, object);

non-static Equals

The base implementation of the non-static object.Equals method, which is used with most reference types, matches that of ReferenceEquals. In string and certain other framework classes, it is overridden to compare instance content instead:

public virtual bool Equals(object);

The method is overridden in ValueType to compare the type and content of the instances. Reflection is used to find members introduced in user types, so the default ValueType implementation is somewhat slow. It is commonly overridden in value types to improve performance.

Equality operator

By default, the equality operator acts much like the non-static Equals method: most reference types are compared by address, string and all value types are compared by content, and value type comparisons use reflection. Unlike calls to non-static Equals, equality operator calls are bound statically, as are all calls to static methods.

The equality operators are commonly redefined in value types to improve performance. Redefining the equality operator entails redefining the inequality operator as well.

Equality with value types

To avoid reflection, the non-static Equals method is usually overloaded, and the equality operators redefined in value types:

struct tPt {
  public int X, Y;

  public override bool Equals(object aq) {
    if (aq == null) return false;
    if (aq.GetType() != GetType()) return false;

    tPt oPt = (tPt)aq;
    return (oPt.X == X) && (oPt.Y == Y);
  }

  public static bool operator==(tPt aPt0, tPt aPt1) {
    return (aPt0.X == aPt1.X) && (aPt0.Y == aPt1.Y);
  }

  public static bool operator!=(tPt aPt0, tPt aPt1) {
    return !(aPt0 == aPt1);
  }

  public override int GetHashCode() {
    ...
  }
}

Replacing the non-static Equals method, the equality operator, or the inequality operator generally entails that all three be replaced, along with GetHashCode.

Inheritance

A C# class may specify at most one parent class.

Structures inherit implicitly from ValueType. They cannot inherit or be inherited, but they can implement interfaces.

base

Parent class implementations, whether hidden or overridden, can be referenced with the base keyword:

class tqQue {
  public virtual void vAdd() {
    ...

class tqQueDbl: tqQue {
  public override void vAdd() {
    base.vAdd();
    ...

new

If a class introduces a member that shares a name with some member of its parent class, and if the new member is not declared override:

class tqMgr {
  public int Cd;
}

class tqMgrStd: tqMgr {
  public bool Cd;
}

a compiler warning will result, and the new member will hide the original when the name is referenced through the child:

tqMgr oqMgr = new tqMgr { Cd = 10 };

tqMgrStd oqMgrStd = new tqMgrStd { Cd = false };
(oqMgrStd as tqMgr).Cd = 20;

The compiler warning can be suppressed by declaring the new member new:

class tqMgrStd: tqMgr {
  new public bool Cd;
}

virtual and override

Properties, indexers, and events can be declared virtual, just like methods:

class tqMgr {
  public virtual int vTop { get; set; }
}

Both accessors must be specified when an event is overridden. If only one accessor is specified in a property or indexer override, the other will function as before, so these cannot be made read-only or write-only by omitting an accessor:

class tqMgrCk: tqMgr {
  int eCtRead;

  public override int vTop {
    get {
      ++eCtRead;
      return base.vTop;
    }
  }
}

Neither can any member's access level be changed when it is overridden.

Because structures do not support inheritance, they cannot define virtual members.

abstract

Classes declared abstract cannot be instantiated. They can define members that are abstract or concrete.

Methods, properties, indexers, and events can be declared abstract, so long as they are declared within an abstract class. These provide no implementations, and are implicitly virtual:

abstract class tqiTbl {
  public abstract bool vNext();
  public abstract object vVal { get; }
}

sealed

Classes that are declared sealed cannot be subclassed:

sealed class tqMgrSumm: tqMgr {
  public override int Cd() { return 30; }
}

Overrides declared sealed cannot be further overridden by any child class:

class tqMgrStd: tqMgr {
  public sealed override int Cd() { return 20; }
}

Interfaces

Interfaces can contain methods, properties, indexers, or events. To implement an interface, a class or structure must publicly implement all members of that interface:

interface fi {
  bool Ck { get; }
  void Next();
}

class tqiRev: fi {
  public bool Ck {
    get { ... }
  }
  public void Next() { ... }
}

Classes and structures can implement multiple interfaces. An interface that inherits from another interface gains all members of that parent:

interface fiRnd: fi {
  void Jump(int aIdx);
}

Interfaces can inherit from multiple parents.

An instance can be implicitly cast to any interface it implements:

tqiRev oqiRev = new tqiRev();
fi oqi = oqiRev;

Explicit interface implementation

If the same name is used by members of different interfaces:

interface fi {
  bool Ck { get; }
  ...

interface fHand {
  bool Ck(int aIdx);
  ...

and if those interfaces are implemented by the same type, all but one of the conflicting members must be explicitly implemented:

class tqiHand: fi, fHand {
  public bool Ck {
    get { ... }
  }

  bool fHand.Ck(int aIdx) { ... }
  ...

Instances with explicitly implemented members must be cast to the relevant interface before those members can be used:

tqiHand oqiHand = new tqiHand();
bool oCkEls = oqiHand.Ck;
bool oCkHand = ((fHand)oqiHand).Ck(0);

If all other conflicting members are implemented explicitly, the last one can be implemented implicitly, since this will produce no ambiguity. Members can also be implemented explicitly even when there is no conflict, and this is sometimes done to 'hide' certain members.

Explicitly implemented members cannot bear access modifiers; like all interface members, they are necessarily public. Unlike implicitly implemented members, they cannot be declared new or virtual.

Interface reimplementation

Implicit interface implementations can be declared virtual. When this is done, polymorphic behavior is obtained whether a method is invoked through the interface or through the instance type.

Explicit interface implementations cannot be declared virtual. All implementations, explicit or otherwise, can be reimplemented in descendant classes, however:

interface fFilt {
  int Ct();
}

class tqFilt: fFilt {
  int fFilt.Ct() { ... }
}

// Reimplement fFilt:
class tqFiltAll: tqFilt, fFilt {
  int fFilt.Ct() { ... }
}

This does not provide polymorphic behavior when the method is invoked through an instance type, but when it is invoked through the interface, the most recent implementation is used:

tqFilt oqFilt = new tqFiltAll();
int oCt = ((fFilt)oqFilt).Ct();

Generics

Generics allow code to be reused without casting or unnecessary boxing. Classes, structures, interfaces, methods, and delegates can be defined as generics.

Several generics can share the same name so long as they accept different numbers of generic arguments:

static void eExec<xz>(xzKey azKey) {
  ...

static void eExec<xzKey, xzData>(xzKey azKey, xzData azData) {
  ...

Generic classes and structures

The type parameters in a generic class or structure are available throughout the type definition:

struct tPt<xz> {
  public tPt(xz azX, xz azY) {
    zX = azX;
    zY = azY;
  }

  public xz zX, zY;
}

The generic must be specialized when it is used:

static void Main() {
  tPt<byte> oPt = new tPt<byte>(1, 2);
  ...

Generic classes can be subclassed as usual. When this is done, type parameters can be specialized in the parent:

class tqContainPt: tqContain<tPt> {
  ...

or they can be left open, to be specialized with the child:

class tqContainCache<xEl>: tqContain<xEl> {
  ...

When static data is defined within a generic:

class tqStore<xEl> {
  public static int sCt;
  ...

separate instances of that data are maintained for each specialization:

eRep(tqStore<tBlock>.sCt, tqStore<tLine>.sCt);

Generic interfaces

Interfaces can also be generic:

interface fSet<xz> {
  void Add(xz az);

  xz this[int aIdx] { get; }
}

class tqSet: fSet<byte> {
  public void Add(byte a) {
    ...

  public byte this[int aIdx] {
    get { ... }
  }
}

Generic methods

A generic method introduces a new type parameter that is defined only within that method:

public static void sSwap<xz>(ref xz arz0, ref xz arz1) {
  xz oz0 = arz0;
  arz0 = arz1;
  arz1 = oz0;
}

When such a method is called, the generic argument list can be omitted if the target is unambiguous:

sSwap(ref oPt1, ref oPt2);

Otherwise, it must be specified:

sSwap<tPt<byte>>(ref oPt1, ref oPt2);

Generic delegates

Delegates can also be generic:

delegate xz tdExt<xz>(xz az);

static tPt eExtNext(tPt aPt) {
  ...

static void Main() {
  tdExt<tPt> odExt = eExtNext;
  tPt oPt = odExt(new tPt(1, 2));
  ...

Generic constraints

Constraints limit the types that can be used to specialize a given generic parameter. No member of a parameter type can be accessed within the generic unless a constraint guarantees its presence.

Constraints are defined with the where keyword. In classes, structures, and interfaces, they are specified just after the type parameter list:

struct tSet: fSet {
  public void Add(byte a) {
    ...

class tqFiltAll: tqFilt {
  public override bool vCk() {
  ...

class tqHand<xqData, xqFilt>
  where xqData: fSet, new()
  where xqFilt: tqFilt {

  public tqHand(xqData aqData, xqFilt aqFilt) {
    if (aqFilt.vCk()) qData.Add(1);
  }

  public void Reset() {
    qData = new xqData();
    ...

In methods and delegates, they are specified at the end of the declaration:

delegate void dFlip<x>(x a)
  where x: struct;

Multiple constraints can be applied to a single parameter.

class and struct constraints

Specifying class or struct mandates that the parameter type be a class or a structure.

Base class constraints

Specifying a specific class mandates that the parameter type match that class, or a subclass thereof. The class can itself be specified with another type parameter, which is known as a naked type constraint:

interface fTbl<xqEl> {
  List<xqElCd> ElsCd<xqElCd>(byte Cd)
    where xqElCd: xqEl;
}

Interface constraints

Specifying an interface mandates that the parameter type implement that interface.

Parameterless constructor constraints

Specifying new() mandates that the parameter type provide a parameterless constructor.

unmanaged constraints

Starting with C# 7.3, specifying unmanaged mandates that the parameter type be a boolean, character, or numeric type, or an enum, or a non-generic structure that contains only such types.

Type argument variance

Although a child class instance can be assigned to a parent class reference:

class tqTask {
  ...

class tqSync: tqTask {
  ...

void eReady() {
  tqTask oqTask = new tqSync();
  ...

it is not normally possible to assign a generic instance that is specialized with a child class to an interface that is specialized with the parent:

void eAdd(fView<tqSync> afViewSync) {
  fView<tqTask> ofView = afViewSync;
  ...

If a given type parameter is only used as a return type, it can be marked out in the type parameter list, making it covariant:

interface fView<out x> {
  x Copy();
  ...

In general, a generic instance accepts or returns instances of the argument type, which in this case is a child class. That instance is here referenced through an interface, which is specialized with a parent class. out guarantees that this particular type is only returned from the generic instance, so the child class in the instance is always referenced by the parent class in the interface. The preceding assignment therefore becomes valid.

If some type parameter is only used as an input type, it can be marked in, making it contravariant:

interface fWorker<in x> {
  void Que(x aWork);
  ...

In this case, data flows not from instance to interface, but from interface to instance, so any inheritance relationship must be reversed. This makes it possible to assign a generic instance that is specialized with the parent to an interface that is specialized with the child:

void eHire(fWorker<tqTask> afWorker) {
  fWorker<tqSync> ofWorkerSync = afWorker;
  ...

The result is an interface that accepts values of a specific type and forwards them to an implementation that expects a general type.

Only interfaces and delegates can use generic type argument variance. Delegates provide some type variance by default:

delegate tqTask tdTaskFromSync(tqSync aqSync);

tqSync eSyncFromTask(tqTask aqTask) {
  ...

void eExec() {
  tdTaskFromSync odTask = eSyncFromTask;
  ...

This works when a method is directly assigned to a delegate, whether the delegate is generic or not. However, if a delegate instance is assigned to another delegate, the variance must be explicitly defined:

delegate xOut tdTaskAssoc<out xOut, in xIn>(xIn aqTask);

void eStart() {
  tdTaskAssoc<tqSync, tqTask> odTaskDef = eSyncFromTask;
  tdTaskAssoc<tqTask, tqSync> odTaskNext = odTaskDef;
  ...

Although it is possible to copy an instance between different specializations of the same generic delegate type, it is never possible to copy between different types, even if their signatures match.

default

The default operator returns the default value of the specified type. It can be applied to predefined or user types.

Originally, it was necessary to pass the type to the operator:

tPt<byte> oPt = default(tPt<byte>);

Starting with C# 7.1, however, the type can be inferred:

tPt<byte> oPt = default;

Type conversion

In general, implicit conversions are allowed only when the compiler verifies that no information can be lost; most other conversions require a cast. Integers are allowed to lose precision when implicitly converted to floating point numbers, however:

float o = 123456789;

Single-parameter constructors do not implement implicit conversions as they do in C++.

Casts

When an attempt is made to perform a fundamentally invalid cast, as when an instance is cast to an unrelated class, a compile-time error results. If the problem cannot be recognized at compile time, as when an instance is downcast to a sibling of the dynamic type, an InvalidCastException is thrown.

as

Like other casts, as generates a compiler error when an attempt is made to perform a fundamentally invalid cast. If the cast fails at run time, as returns null, and no exception is thrown:

oqOutFile = oqOut as tqOutFile;
if (oqOutFile != null) {
  ...

as can be used only with reference types and nullable value types.

is

The is operator returns true if an object can be cast to the specified type:

if (oqOut is tqOutFile) oqOutFile = (tqOutFile)oqOut;

or interface:

void eBind(object aqIn) {
  if (aqIn is fOp) {
    fOp ofOp = (fOp)aqIn;
    ...

Starting in C# 7.0, a variable can be defined within is the expression, by adding a name after the type:

object oObj;
...
if (oObj is int oCt) {
  cTtl += oCt;
  ...

If is returns true, the value will be converted and stored.

Conversion operators

Conversions are implemented with conversion operators, which can convert to or from the types that define them. The conversion parameter determines the source type. The type following the operator keyword determines the destination type:

struct tSpan {
  // Convert from tSpan to Int32:
  public static implicit operator Int32(tSpan aSpan) {
    return aSpan.Wth;
  }

  // Convert from Int32 to tSpan:
  public static explicit operator tSpan(Int32 a) {
    return new tSpan(0, a);
  }

  public tSpan(Int32 aLeft, Int32 aWth) {
    Left = aLeft;
    Wth = aWth;
  }

  public Int32 Left, Wth;
}

If a conversion is marked explicit, it must be performed with a cast:

tSpan oSpan = (tSpan)10;

If it is marked implicit, it is performed automatically:

Int32 oWth = oSpan;

true and false operators

Types can define true and false operators:

struct tPt {
  public static bool operator true(tPt aPt) {
    return (aPt.X != 0) || (aPt.Y != 0);
  }

  public static bool operator false(tPt aPt) {
    return (aPt.X == 0) && (aPt.Y == 0);
  }
  ...

allowing instances to be evaluated within if, do, while, for, and ? statements without defining an implicit conversion to bool:

void ePlot(tPt aPt) {
  if (aPt) {
    ...

If either operator is implemented, both must be implemented. The operators do not support implicit or even explicit conversion to bool.

Implementing true and false allows a type representing ternary logic values to be used within control statements, as it can define itself to be simultaneously neither true nor false. Nullable bool types provide a simpler solution to this problem.

Namespaces

Namespaces define distinct, named scopes:

namespace nMath {
  struct tPt {
    public float X, Y;
  }

  namespace nPolar {
    struct tPt {
      public float R, A;
    }
  }
}

Namespaces can span units and even assemblies. Members are added to an existing namespace by including them in a new namespace block:

namespace nMath {
  namespace nTrig {
    ...

Nested namespaces can be defined from outside the parent namespaces, even when one or more intermediate namespaces have yet to be defined:

namespace nMath.nRnd.nMersenne {
  class tqGen {
    public Int32 zInt32() {
      ...

Any namespaces or types not explicitly defined within another namespace are part of the global namespace. Note that global is suffixed with two colons, not a period:

global::System.Console.Write("Done");

Members of parent namespaces are automatically accessible from child namespaces. Nested namespaces can reference sibling namespaces without qualifying their common parent:

namespace nMath.nRnd.nTrait {
  interface fTrait {
    ...

namespace nMath.nRnd.nMersenne {
  class tqTrait: nTrait.fTrait {
    ...

using directives

The using directive allows the members of one namespace to be referenced from a different namespace without fully qualifying the members each time. However, if the same name is found within several used namespaces, it will be necessary to qualify them at least partially.

Directives must be placed at the beginning of a namespace:

namespace tPhys {
  using nMath;

  struct tProj {
    public tPt Pos;
    ...

or at the top of the unit, if they are in the global namespace.

When placed in the global namespace, the directive's effect is limited to the containing unit; when placed in any other namespace, it is limited to that namespace block. Other blocks that extend the same namespace do not benefit from the directive:

namespace nComm {
  class tqBuff {
   ...
}

namespace nDisp {
  using nComm;

  class tqConsole {
    public tqConsole() {
      tqBuff oqBuff = new tqBuff();
      ...
}

namespace nDisp {
  class tqList {
    public tqList() {
      nComm.tqBuff oqBuff = new nComm.tqBuff();
      ...

Directives can precede the definitions of the namespaces they reference:

using nMath;

namespace nMath {
  ...

using aliases

using can also be used to alias namespaces:

using nRndMersenne = nMath.nRnd.nMersenne;

or to alias namespace members:

using fTraitRnd = nMath.nRnd.nTrait.fTrait;

Like using directives, alias definitions must be placed at the beginning of a unit or a namespace. They are allowed to reference namespaces or namespace members that have yet to be defined.

Variables

Variable declaration

No local variable can share a name with another variable in a parent local scope. Names can be shared with variables in the containing class scope, however:

class C {
  Int32 X = 0;

  void F() {
    Int16 X = 1;
    ...

It is thus possible to hide class variables with local variables, but it is not possible to hide local variables with other locals.

If several local or class variables share the same type and qualifiers, they can be declared together:

int oCtMin = 1, oCt;

A value can be assigned to a discard to explicitly show that it is not needed:

_ = Upd();

var

If the type of a local variable can be inferred from its initializer expression, that type can be replaced in the variable declaration with var:

var oqMsg = "Done";

var cannot be used outside of local variable declarations.

dynamic

Ordinarily, when a method or other member is referenced through an instance of some type, that reference is resolved at compile time. If the referenced element is not found in the type, a compile-time error results.

If the instance is assigned to a dynamic variable, dynamic binding is used instead. Members are accessed or invoked just as before, but these references are resolved at run time. If the resolution fails, a RuntimeBinderException is thrown:

object eSurf() {
  ...

void eRefresh() {
  dynamic oqSurf = eSurf();
  oqSurf.Reset();
  ..

If the instance implements IDynamicMetaObjectProvider, members of that interface will be invoked when the dynamic binding is resolved. These members can forward the reference to specific elements in the dynamic type, or they can process the reference themselves, allowing client code to access elements that aren't even defined in the type. If IDynamicMetaObjectProvider is not implemented, the reference will be resolved much the way it would if static binding had been used.

All conversions to or from dynamic are implicit:

dynamic oqData = new List<int>() { 1001, 1002, 1004 };
List<int> oqCds = oqData;

If its parameters are declared dynamic, a single function can be used with any number of different types, though any binding errors will not be detected until run time:

static dynamic Min(dynamic a0, dynamic a1) {
  if (a0 <= a1) return a0;
  return a1;
}
Limitations of dynamic

Some functions cannot be invoked dynamically. Extension methods are bound at compile time by comparing the calling signature against methods that are in scope where the call is made. This cannot be done at run time because such information is lost after the code is compiled.

When a type explicity implements an interface member:

interface fCache {
  void Reset();
  ...

class tqCache: fCache {
  void fCache.Reset() {
    ...

it becomes impossible to access that member without first casting to the interface:

void Upd(tqCache aqCache) {
  fCache ofCache = aqCache;
  ofCache.Reset();
  ...

Casting to an interface does not change the type of the instance, however. If the interface is assigned to a dynamic variable, all run time knowledge of the interface will be lost, and any direct attempt to use the explicitly-implemented member will produce a run time error:

dynamic oqObj = ofCache;
oqObj.Reset();

To access the explicit implementation, the instance must be re-cast:

(oqObj as fCache).Reset();

Similarly, dynamic binding cannot be used to invoke a base implementation because information about the parent class will be unavailable at run time:

class tqBuffFast: tqBuff {
  public override void Add(dynamic aqData) {
    base.Add(aqData);
    ...

In this case, the compiler will know that the call cannot be completed, and it will produce a compile time error.

Variable initialization

Local variables cannot be read until they have been assigned. Class and structure variables are default-initialized if they are not explicitly initialized, and can be used at any time.

Like local variables, class variables can be assigned with initializers:

class tqMgrOver {
  int eCt = 3;
  ...

Structure variables cannot be initialized this way.

For a given type, variable initialization and construction occurs in this order:

1) Static variable initialization;
2) Static construction;
3) Non-static variable initialization;
4) Non-static construction.

so if a variable is initialized both within its declaration and within a constructor, the constructor assignment will persist. The initialization of specific variables, whether static or non-static, follows the order in which they were declared.

Accessible variables or properties within a class or structure:

class tqSumm {
  public tqSumm() {}
  public tqSumm(int aAct) { Act = aAct; }

  public int Pend, Act;

  public int Comp {
    get { return tqHist.sCt; }
    set { tqHist.sCt = value; }
  }
}

can be assigned after construction with a single statement:

tqSumm oqSumm = new tqSumm(4) {
  Pend = 1,
  Comp = 12
};

Because the assignments are named, they can be specified in any order. If the default constructor is used, its parentheses can be omitted:

tqSumm oqSummNext = new tqSumm { Act = 2 };

Though this can be done only at construction time, it is not initialization per se. Read-only variables cannot be assigned this way, and declaration and constructor initializations are still performed, though their values are overwritten by the assignment.

const

Local or class variables declared const cannot be modified after they are initialized. Their values are calculated at compile time, so they must be initialized with constant expressions:

const int oTicksPerMin = 60 * 1000;

readonly

Class variables declared readonly cannot be modified after they are initialized. If their default value is not to persist, they must be assigned with initializers:

readonly int eCtMax = 10;

or within the body of a constructor:

public tqMgr() {
  eCtMax = 10;
}

Local variables cannot be declared readonly.

The same readonly variable can be assigned more than once within a constructor. As with other variables, if a readonly variable is initialized within its declaration and within a constructor, the constructor assignment will persist.

Unlike const variables, readonly variables can be initialized with non-constant expressions.

Null values

Nullable types

System.Nullable<val> wraps value types in a structure that is able to represent null values. Nullable types are created by specializing Nullable<val>, or by marking value types with ?:

int? oLen = null;

Values can be directly assigned to nullable types:

oLen = 10;

To extract an instance of the original type, the Value property must be invoked:

int oDist = oLen.Value;

or the instance must be explicitly cast:

int oDist = (int)oLen;

If an attempt is made to extract the original type from a null instance, InvalidOperationException will be thrown. The nullable instance can always be compared with non-nullable types:

if (oLen == oLenMax) ...
else if (oLen > oLenMax) ...

All such comparisons return false if the instance is null.

The null-coalescing operator ?? can be used to return an alternative value:

int oDist = oLen ?? 0;

or the Nullable<val>.GetValueOrDefault method can be called, this returning the default value for the base type if the instance is null.

Nullable<val> cannot be specialized with classes, as these are inherently nullable. Boxed value types are also inherently nullable, so when nullable types are boxed, only the underlying value type is copied to the heap. The as operator can be used to unbox an instance as a nullable type; when this is done, the nullable stores the instance if the types match, or null if they do not:

void eSend(object aqData) {
  Int32? oInt32 = aqData as Int32?;
  if (oInt32.HasValue) {
    ...
Operator lifting

Nullable<val> does not implement the equality, comparison, or other operators that value types commonly do. After ensuring that the operand is non-null, the compiler forwards such calls to operators in the underlying type. This operator lifting allows nullables to be compared directly with equivalent non-nullable variables.

Null values, when encountered, are handled differently by different operators.

Nullable<val> equality operators return true if both instances are null, or if neither is null and the lifted operator returns true. Nullable<val> inequality operators return true if only one operand is null, or if neither is null and the lifted operator returns true.

Nullable<val> comparison operators return false if any operand is null; otherwise, they defer to the lifted operator. This allows complementary comparison operators to return false for the same set of operands.

With one exception, all other Nullable<val> operators, including the arithmetic and bitwise operators, return null if any operand is null. When Nullable<val> is specialized with bool, the bitwise logical operators treat null as an 'unknown' value.

Operator Operand Operand Result
operator& null null null
null true null
null false false
operator| null null null
null true true
null false null
operator^ null null null
null true null
null false null

The 'short-circuiting' logical operators cannot be explicitly overloaded, and therefore cannot be used with Nullable<val>.

Null-coalescing operator

The null-coalescing operator ?? accepts two operands, the first of which must be a reference or a nullable type. If that operand is non-null, its value is returned; otherwise, the second operand is:

void eWrite(string aqText) {
  Console.WriteLine(aqText ?? "BLANK");
  ...

Null-conditional operators

The ?. operator conditionally dereferences a class member. When a class variable is dereferenced, its value is returned if the reference is non-null, otherwise null is returned. This requires that the result be stored in a reference type:

void eNext(tEl aqEl) {
  string oqName = aqEl?.qName;
  ...

or in a nullable value type:

int? oNum = aqEl?.Num;

The same rules apply if a class function or property is dereferenced, with these being executed only if the reference is non-null.

The ?[] operator conditionally dereference an indexer:

void eAdd(int[] aqIdxs) {
  int? oIdxFirst = aqIdxs?[0];
  ...

Statements

goto

goto jumps to a label defined within the same method:

while (true) {
  goto X;
}

X:
Console.WriteLine("Done");

Every label must be followed by a statement or another label. Labels do not have function scope, so goto cannot be used to jump into a block. They may not share names with other labels inside subsidiary blocks, however.

switch

In earlier versions of C#, switch values could be strings, enumerations, or integral values, including bool and char. They could also be nullable variations of those types, with nullable values being automatically lifted to match case values:

int? oOff = eOffNext();
switch (oOff) {
  case null:
    ...
  case 1:
    ...

case expressions were selected by matching their values against the switch variable.

Starting in C# 7.0, any type can be passed to switch. Also, case expressions can be matched against the switch variable's type. When this is done, the case expression can define a new variable that stores the converted value, much like the is operator:

object oObj = cRead();
switch (oObj) {
  case int oCt:
    cTtl += oCt;
    ...

The first case match is selected. The default block is selected only if all case blocks fail to match, however, regardless of its position in the list.

case expressions can also include the when operator, which tests another expression, possibly one that uses the new variable:

switch (oObj) {
  case int oCt when (oCt > 99):
    throw new Exception("Invalid count");
  case int oCt:
    ...

case expressions can also specify var, which matches every type, plus null. Combining this with when allows switch variables to be compared with non-constant values:

switch (oCd) {
  case var oCdMatch when (oCdMatch == cCdDef()):
    ...

Most case blocks must end with an explicit jump; the program is not allowed to 'fall through' to another case unless the first block is entirely empty. Control can be passed to other case blocks (including default) with goto, but only if the destination case matches a constant:

switch (oCd) {
  case 'A':
    if (oIdx > 2) goto default;
    break;
  case 'B':
    if (oLvl < 4) goto case 'A';
    return 'B';
  case 'X':
  default:
    return 'X';
}

goto can also jump to a label outside the switch statement. Other valid jump statements include break, continue, return, and throw.

foreach

Any type implementing IEnumerable or IEnumerable<el> can be iterated with foreach:

foreach (char oCh in "iron")
  Console.Write((char)(oCh + 1));

Iterators

IEnumerator and IEnumerator<el> describe forward-only cursors that traverse a sequence of values. Enumerators are types that implement these interfaces or that define the Current and MoveNext members therein. They are used as visitor instances within foreach implementations.

Enumerables are types that implement IEnumerable or IEnumerable<el>, or that define a GetEnumerator method that returns an enumerator, as IEnumerable specifies. The enumerable's ability to generate enumerators allows it to be used with foreach.

Iterators are methods, properties, or indexers that return sequence values with yield statements, and that return these through IEnumerator or IEnumerable interfaces.

GetEnumerator can be implemented as an iterator:

class tqSet: IEnumerable {
  public IEnumerator GetEnumerator() {
    yield return 10;
    yield return 18;
  }
}

allowing the containing type to be passed to foreach:

tqSet oqSet = new tqSet();
foreach (int o in oqSet)
  ...

Iterators that return IEnumerable:

IEnumerable<string> eEls(char aCh) {
  if (aCh < ' ')
    throw new Exception("Invalid start");

  if ((aCh < 'A') || (aCh > 'Z')) yield break;

  for (char oCh = aCh; oCh <= 'Z'; ++oCh)
    yield return oCh.ToString();

  yield return "Done";
}

can themselves be passed to foreach. The compiler uses them to create backing classes that implement IEnumerable<el> and IEnumerator<el>:

foreach (string oq in eEls('X')) {
  ...

Each iterator invocation corresponds to a MoveNext call on the backing enumerator. State within an iterator is extended to the lifetime of the active foreach, which continues until the iterator reaches its end, or until yield break is called.

Note that simply creating an iterator:

var oEls = eEls('X');

does not cause its implementation to be invoked. In particular, if the implementation validates its arguments, that validation will not occur until the first iteration:

foreach (var oEl in oEls)
  ...

If validation is meant to occur before iteration, it must be performed in a method that does not yield. This can be accomplished by iterating in a local function:

IEnumerable<string> eEls(char aCh) {
  if (aCh < ' ')
    throw new Exception("Invalid start");

  return oEls(aCh);

  IEnumerable<string> oEls(char aCh) {
    if ((aCh < 'A') || (aCh > 'Z'))
      yield break;

    for (char oCh = aCh; oCh <= 'Z'; ++oCh)
      yield return oCh.ToString();

    yield return "Done";
  }
}

break

break jumps out of the enclosing for, while, do while, or switch statement. Only one such statement is terminated.

continue

continue effectively jumps to the end of the enclosing for, while, or do while statement, not the beginning. The distinction is important for do while loops, where continue causes the loop condition to be retested.

Delegates

After declaring a delegate type:

delegate bool tdCk(string aqName);

a delegate variable can be defined, and a delegate instance created and assigned to that variable:

bool eCk(string aqName) {
  ...

void eReady() {
  tdCk odCk = new tdCk(eCk);
  ...

Delegate instances are created implicitly by assigning a method directly to the variable:

tdCk odCk = eCk;

or by adding a method to a null variable:

tdCk odCk = null;
odCk += eCk;

The referenced method can then be invoked through the variable, as though the variable were a function:

bool oCk = false;
if (odCk != null) {
  oCk = odCk("Echo");
  ...

with this being equivalent to:

oCk = odCk.Invoke("Echo");

Invoking a null variable causes a NullReferenceException to be thrown.

Delegate compatibility

An instance can be created with any method that matches the delegate's signature, regardless of the method's access level, or whether it is static. When the delegate instance is made to reference a non-static method, the class or structure instance containing that method is stored in the delegate's Target property. When a static method is referenced, Target is null.

Instances can be copied from one delegate reference to another, but the references must have the same type:

tdExt odExt = eExtTop;
tdExt odExtNext = odExt;

Instances can never be copied from one delegate type to another, even when the delegates share the same signature. If the signatures are compatible, a new delegate instance can be created from the original:

delegate void tdExt();
delegate void tdExtDev();

void eExtFile() {
  ...

void eInit() {
  tdExtDev odExtDev = eExtFile;
  tdExt odExtDef = new tdExt(odExtDev);
  ...

Parameters pass from the delegate to the referenced method. When methods are assigned directly to delegates, parameter types contravary by default. This allows methods with more general parameter types to be assigned to delegates with more specific types. Arguments that meet the delegate's requirements will necessarily meet those of the method.

Return values pass from the method back to the delegate. When methods are assigned directly to delegates, these types covary by default. This allows delegates to be assigned with methods that have more specific return types, since return values that meet the method's requirement will necessarily meet that of the delegate.

When instances are copied between different specializations of the same generic delegate type, it becomes necessary to define type variances explicitly with in and out.

Multicasting

A single delegate variable:

tdExec odExec = eExecBack;

can reference and invoke multiple methods:

odExec += eExecBord;
odExec += eExecFill;
odExec();

Such methods are invoked in the order in which they were added. Though all methods are invoked, only the last result is returned.

Specific methods can be removed individually:

odExec -= eExecStd;

The entire list can be cleared by assigning with null, or replaced by assigning a new instance:

odExec = eExecAll;

Events

After a delegate variable is marked with event:

class tqCast {
  public event tdNote dAdd_Note;

  public void Add(int aIdx) {
    if (dAdd_Note != null) dAdd_Note(aIdx);
    ...

methods outside the containing class are no longer able to invoke the delegate or modify it, except with operator+= and operator-=:

static void eNote(int aIdx) {
  ...

public static void Main() {
  tqCast oqCast = new tqCast();
  oqCast.dAdd_Note += eNote;
  ...

Some developers use the null-conditional operator to check for null before calling the delegate's Invoke method directly:

public void Add(int aIdx) {
  dAdd_Note?.Invoke(aIdx);
  ...

This implicitly copies the event before the check, which prevents it from being cleared between the check and the invocation. Real thread safety will often require more comprehensive treatment, however.

Events can be static, abstract, virtual, overridden, or sealed.

By default, events are implemented with a hidden backing variable and accessors, much the way automatic properties are. These can also be defined explicitly:

tdNote edAdd_Note;
public event tdNote dAdd_Note {
  add { edAdd_Note += value; }
  remove { edAdd_Note -= value; }
}

When this is done, even the containing class is unable to invoke the event or modify it, except with operator+= and operator-=. The class instead must manipulate the backing variable. When an interface event is explicitly implemented, its accessors must be defined this way.

Lambda expressions

Lambda expressions are unnamed methods that are used to create delegate instances or expression trees. They consist of a parameter list and an expression or statement block joined by the lambda operator. The parameter list and expression or return type define the signature of the lambda expression:

delegate char tdChar(byte aIdx);

public static void Main() {
  tdChar od = (byte oIdx) => (char)(oIdx + 32);
  ...

If no parameters are defined:

delegate void tdExec();

an empty parameter list must be specified:

tdExec od = () => { ++oCt; }

If a single parameter is defined, and if its type can be inferred, the type and the parameter list parentheses can be omitted:

tdChar od = oIdx => (char)(oIdx + 32);

A set of generic delegates are defined within the System namespace to facilitate the creation of lambda expressions. The Action delegate is overloaded to accept up to sixteen parameters with distinct types:

Action<string> odWrite = o => Console.WriteLine(o);

Func also accepts up to sixteen types, but it also returns a type, which is specified at the end of the type argument list:

Func<byte, char> odToCh = o => (char)(o + 32);

Predicate is similar, but it always returns bool:

Predicate<int> odCkOdd = o => ((o % 2) != 0);

Outer variables

Variables in the scope defining a lambda expression can be read or written from within the expression:

delegate float tdOp(float a);

class tqArith {
  public static float sDenom = 3;

  public tdOp dOp = o => o / sDenom;
}

These are called outer or captured variables. Methods that make use of outer variables are called closures. Outer variables are evaluated when the lambda expression is invoked, not when it is defined:

tqArith oqArith = new tqArith();
tqArith.sDenom = 4;
float oQuot = oqArith.dOp(2);

When a variable is captured by a lambda expression, its lifetime is extended to that of the delegate instance storing the expression. Even local variables are extended this way:

delegate string tdCd(Int16 aX, Int16 aY);

class tqFmt {
  public tdCd dCd;

  public tqFmt() {
    int oCt = 0;

    dCd = (Int16 aX, Int16 aY) => {
      ++oCt;
      return string.Format("{0}: {1}/{2}", oCt, aX, aY);
    };
  }
}

Anonymous methods

An anonymous method is similar to a lambda expression. It is defined much like any other method, but its name and return type are replaced with the delegate keyword. The definition is then assigned to a delegate variable:

delegate byte tdEncr(byte a);

void eStart() {
  tdEncr od = delegate (byte a) {
    return (byte)(a ^ 0xAA);
  };
  ...

If the parameters are not needed by the anonymous method, the parameter list can be omitted, even if the delegate signature includes parameters:

od = delegate { return 0; };

Outer variables function as they do in lambda expressions. Implicit parameter typing is not supported, and the method must be defined with a complete statement block.

Multithreading

Creating threads

Thread class

A thread can be created by instantiating the Thread class within System.Threading. The Thread constructor accepts a ThreadStart or a ParameterizedThreadStart delegate. The first of these requires no parameters and returns void:

void eWork() {
  ...

void eExec() {
  Thread oqThr = new Thread(eWork);
  oqThr.Start();
  ...

while the second expects a single object parameter:

void eWorkPart(object aqData) {
  ...

void eExec(object aqData) {
  Thread oqThr = new Thread(eWorkPart);
  oqThr.Start(aqData);
  ...

The thread is started with one of the Start overloads, one of which accepts the object required by the ParameterizedThreadStart delegate. By default, the Thread class creates a foreground thread, which keeps the process alive as long as it is running. Setting IsBackground to true changes this to a background thread, which is aborted automatically when the last foreground thread terminates.

One thread can wait for another to complete by invoking one of the Join overloads, which wait indefinitely, or for a certain TimeSpan, or a number of milliseconds. If the UI thread is made to wait, it will continue to process the Windows message queue.

Threads can be stopped temporarily with Suspend, and restarted with Resume. A thread can usually be terminated by calling the Abort method, which raises a ThreadAbortException exception within the thread. This exception can be caught by the work method, but it is automatically rethrown at the end of any catch unless ResetAbort is called.

Thread pooling

To reduce startup time, threads can be borrowed from the ThreadPool class in System.Threading. This is done by passing a WaitCallback delegate to one of the static QueueUserWorkItem overloads. WaitCallback expects an object parameter and returns void. One of the QueueUserWorkItem overloads allows an object to be forwarded to the work function:

object oqData = eData();
ThreadPool.QueueUserWorkItem(eWorkTarg, oqData);

while the other passes null:

ThreadPool.QueueUserWorkItem(eWorkTarg);

All ThreadPool threads are background threads.

volatile

Class and structure variables can be declared volatile; this warns the compiler that the value could be changed from outside a given thread:

class tqMgrComm {
  volatile int eCtHost;
  ...

Unlike C++, C# does not allow methods or local variables to be volatile.

lock

Given an instance of some reference type:

object eqLock = new object();

lock obtains a mutex for the instance, releasing it only when the following block is complete:

lock (eqLock) {
  ...
}

This is implemented by the compiler as:

System.Threading.Monitor.Enter(eqLock);
try {
  ...
}
finally {
  System.Threading.Monitor.Exit(eqLock);
}

Value types must not be passed to lock. Because a reference type is expected, any value type would be boxed, and the lock would always fail to engage during subsequent checks.

It is generally inadvisible to pass this or the result of a typeof call to lock, as neither technique is completely encapsulated. A class that locks its own instance may be broken if its client code locks that same instance. Locking the static instance returned by typeof presents a similar problem.

Asynchronous programming

Tasks

The task classes in System.Threading.Tasks are used to represent asynchronous operations. Task<result> references an operation that returns type result, while Task returns nothing. A task can be created by passing a delegate or lambda expression to one of the static Task.Run or Task.Run<result> overloads:

float eLoadAvg(string aqNameServ) {
  ...
}

Task<float> eStart_LoadAvg(string aqNameServ) {
  return Task.Run(() => LoadAvg(aqNameServ));
}

Task.Run assigns the work to the thread pool; the task itself is returned immediately. Note that the Task.Run overload and specialization is inferred from the return type of the lambda expression. In this case, that causes Task<result> to be returned.

The Task.Delay overloads return tasks that complete after a TimeSpan or a number of milliseconds; no work is performed. This provides an easy way to execute a continuation after a delay.

async and await

Though client code can perform other work after creating the task, it may at some point be necessary to wait for the operation to complete. This is done by applying the await keyword to the task:

async void ePrint_LoadAvgAsync() {
  Task<float> oqTask = eStart_LoadAvg("GAMMA");
  ...
  float oLoad = await oqTask;
  WriteLine(oLoad);
}

Every method that uses await must itself be marked async. Such a method cannot have ref or out parameters. await cannot be used inside the Main function, or inside catch, finally, lock, or unsafe blocks. By convention, async methods are given names that end in 'Async'.

Though the method stops at await if the task is still working, it does not block the thread; instead, it returns to the calling method. Everything after await in the async method is defined as a continuation, to be executed in the same thread context when the task is complete. Though await can cause the instruction pointer to leave the method temporarily, it does not cause finally blocks to be executed. If the task is already complete when await is reached, the rest of the method executes as normal. If a Task<result> is being waited on, await automatically extracts and returns an instance of type result when the task is done. Tasks can also store exceptions, and such are rethrown at await as necessary.

Internally, the compiler implements await by transforming the code into something like:

void ePrint_LoadAvgAsync() {
  Task<float> oqTask = eStart_LoadAvg("GAMMA");
  ...
  TaskAwaiter<float> oAwait = oqTask.GetAwaiter();
  oAwait.OnCompleted(() => {
    float oLoad = oAwait.GetResult();
    WriteLine(oLoad);
  });
}

Asynchronous lambda expressions are created by prefixing the parameter list with async:

Func<int, Task<float>> odAsync = async (int oIdx) => {
  float oUtil = await eStart_Util(oIdx);
  return eUtilAdj(oUtil);
};
...
float oUtilAdj = await odAsync(0);

Asynchronous calls can be chained arbitrarily. If the async method returns a task, other methods can await its work. When this is done, the compiler creates and returns the task instance on its own. So, if a Task<result> is needed, it is enough to return the result:

async Task<float> eLoadAvgOrMinAsync() {
  float oLoad = await eStart_LoadAvg("GAMMA");
  return Math.Min(oLoad, eLoadMin);
}

If a Task is needed, there is no need to return at all:

static async Task eUpd_LoadAvgOrMinAsync() {
  eLoadLast = await eStart_LoadAvg("GAMMA");
}

To execute operations in parallel, create multiple tasks, then perform other work or await the tasks together:

Task oqTask0 = eReset_Serv("DELTA");
Task oqTask1 = eReset_Serv("EPSILON");
...
await oqTask0;
await oqTask1;

Alternatively, pass the tasks to one of the Task.WhenAll or Task.WhenAny overloads to obtain a new combined task:

await Task.WhenAll(oqTask0, oqTask1);

Exceptions

System.Exception provides properties that document the error condition:

  • Source stores the name of the application or assembly that threw the exception;
  • TargetSite returns a MethodBase instance representing the method that threw the exception;
  • StackTrace returns a string representation of the call stack;
  • InnerException returns the exception that caused the current exception, if such was specified in the constructor when this exception was created;
  • Message returns a description of the exception;
  • Data returns an IDictionary reference that can be used to store other data.

throw

Starting with C# 7.0, exceptions can be thrown from expressions, like those created with conditional or null-coalescing operators:

int oIdx = oCkLast
  ? cIdxLast
  : throw new Exception("Cannot get last index");

catch

Only Exception and its descendants can be thrown. To catch all exceptions, specify Exception in the catch block:

catch (Exception aqEx) {
  ...

Only the first block matching a particular exception is executed. If multiple blocks are defined, blocks catching more specific exceptions should be placed first:

catch (DivideByZeroException aqEx) {
  ...
}
catch (Exception aqEx) {
  ...

The exception variable can be omitted if the instance is not needed:

catch (OutOfMemoryException) {
  ...

The type can also be omitted if the instance is not needed, and if all exceptions are to be caught:

catch {
  ...

The original exception can be rethrown by invoking throw without an argument:

catch (Exception aqEx) {
  ...
  throw;
}

This conserves the original stack track, which throw aqEx would not do. Exceptions cannot be rethrown from a finally block.

Exception filters

Adding a when clause to the catch produces an exception filter:

catch (Exception aqEx)
  when (aqEx.InnerException is IOException) {
  ...

The clause can contain any boolean expression. If the expression is found to be false, the exception will not be caught by that block, even if the exception type is a match.

finally

Any jump statement, including continue, break, and goto, can be used to exit a try block, causing the corresponding finally block to be executed immediately. No jump statement but throw can exit a finally block.

Unsafe code

unsafe code can evade certain constraints imposed by the compiler or runtime. This can be used to increase performance, to promote interoperability, or to manage memory outside the managed heap. To compile unsafe code, the /unsafe compiler flag must be set.

Pointer types

C# pointer syntax matches that of C++, with &, *, and -> operators:

tPt oPt = new tPt(1, 2);
tPt* opPt = &oPt;
string oqText = opPt->ToString();

A pointer can reference a value type, an array of value types, a string, or another pointer. It cannot reference other classes or managed types.

As in C++, void pointers are used to reference memory without specifying its type. Such pointers cannot use pointer arithmetic, and cannot be dereferenced. All pointer types can be implicitly cast to void pointers:

tPt oPt = new tPt();
tPt* opPt = &oPt;
void* op = opPt;

void pointers must be explicitly cast to other pointer types:

opPt = (tPt*)op;

unsafe

When a class, structure, or interface is declared unsafe, its members gain the ability to define and use pointers. They cannot be defined or used in other code:

unsafe class tqPack {
  public void Set(Int32* ap) {
    Int32* opOrig = ap;
    ...

Declaring an interface unsafe does not allow implementing types to define or use pointers.

Class, structure, and interface members can be individually declared unsafe. Declaring a variable unsafe allows a pointer to be defined:

unsafe Int32* ep;

Declaring a property, indexer, or method unsafe allows pointers to be used as parameters or return types. It also allows pointers to be defined and used within the member's implementation.

Local variables cannot be individually declared unsafe as class variables can, but entire statement blocks can:

public void Exec() {
  unsafe {
    Int32* op;
    ...

fixed

Classes and other managed types cannot be referenced by pointers. A member of a managed type that is not itself managed can be pointer-referenced, but the instance containing it must be pinned to prevent the garbage collector from moving it while the pointer is in use. This is accomplished with a fixed statement, which synchronizes the pointer's lifetime with that of the pin:

class tqCmd {
  public Int32 ID;
  ...

unsafe class tqPack {
  public void Exec(tqCmd aqCmd) {
    fixed (Int32* opID = &aqCmd.ID) {
      ...

Because strings and arrays are managed, they cannot be referenced by pointers. The content of strings and value-type arrays can be, however. string data is referenced by fixing the string and assigning it to a char pointer:

string oqMsg = "XYX";
fixed (char* op = oqMsg) {
  ...

Value-type array data is referenced by fixing the array and assigning it to a pointer of the relevant type:

Int32[] oqData = { 9, 8, 7 };
fixed (Int32* op = oqData) {
  ...

Pointers can also be assigned with the addresses of specific array elements:

fixed (Int32* op = &oqData[1]) {
  ...

To prevent heap fragmentation, managed allocation should be avoided while instances are pinned, and pins should be released as quickly as possible.

fixed buffers

The fixed keyword is also used to define ranges of unmanaged memory within structures:

unsafe struct tPack {
  public fixed Int16 Data[256];
  ...

Though the definition somewhat resembles that of an array, the buffer size is attached to the name rather than the type, and the result is unrelated to System.Array. fixed buffers are referenced by pointer much as arrays are, but they need not be pinned first:

tPack oPack = new tPack();
Int16* op = oPack.Data;

Specific buffer elements are referenced as they are in arrays:

Int16* op = &oPack.Data[1];

Classes cannot contain fixed buffers.

stackalloc

Within methods, ranges of stack memory can be allocated with stackalloc:

tPt* opPts = stackalloc tPt[8];

Like all stack allocations, this memory is deallocated when the program leaves the containing block. Pointers can be made to reference stackalloc buffers and their elements the same way they are made to reference fixed buffers.

Resource management

using statements

A using statement ensures that a class or structure implementing IDisposable is cleaned up when it goes out of scope:

using (StreamWriter oqStm = File.CreateText(oqPath)) {
  ...
}

This is implemented by the compiler as:

StreamWriter oqStm = File.CreateText(oqPath);
try {
  ...
}
finally {
  if (oqStm != null) ((IDisposable)oqStm).Dispose();
}

Multiple resources can be managed by placing a series of using lines before the same block:

using (StreamWriter oqStmBase = File.CreateText(oqPathBase))
using (StreamWriter oqStmAppd = File.CreateText(oqPathAppd)) {
  ...
}

IDisposable

The IDisposable interface standardizes the management of unmanaged resources. The interface consists of a single method:

public void Dispose();

The implementation is typically made to call a second method that actually releases the resources. The second method is virtual so that subclasses can manage resources through the same mechanism. It always releases unmanaged resources, and, when called from Dispose, it also release any managed resources, so that these can be reused as soon as possible. It then calls GC.SuppressFinalize, since finalization is no longer needed:

class tMgrAud: IDisposable {
  ...

  // IDisposable
  // -----------

  public void Dispose() {
    cDispose(true);
    GC.SuppressFinalize(this);
  }

  bool eCkDisposed = false;

  protected virtual void cDispose(bool aCkManaged) {
    if (eCkDisposed) return;

    // Managed resources
    // -----------------

    if (aCkManaged) {
      eBuffIn.Dispose();
      eBuffOut.Dispose();
      ...
    }

    // Unmanaged resources
    // -------------------

    eRel_Dev();
    ...

    eCkDisposed = true;
  }
}

Dispose can be called more than once, and it must be safe to do so. However, there is no guarantee that it will be called, so if the class is directly responsible for unmanaged resources, it should also implement a finalizer:

~tMgrAud() {
  cDispose(false);
}

The finalizer should not release managed resources, as these may have been released already by the time finalization occurs.

If an attempt is made to use the class after it has been disposed, ObjectDisposedException should be thrown:

public void Exec() {
  if (eCkDisposed)
    throw new ObjectDisposedException("tMgrAud.Exec");
  ...
}

Framework

Collections

Most generic collections are defined within System.Collections.Generic, though ILookup<key, el>, IGrouping<key, el>, and Lookup<key, el> are defined in System.Linq.

Many generic collections are also provided in non-generic forms. These are defined in System.Collections.

Sequence collections

IEnumerable and IEnumerator

IEnumerable<el> describes a sequence or collection that can be enumerated with foreach. It defines one member, GetEnumerator, that returns an IEnumerator<el>.

IEnumerator<el> describes a forward-only cursor that traverses a sequence or collection. It defines a Current property that returns the currently referenced element, and a MoveNext method that advances the cursor. When instantiated, enumerators are set just before the start of the collection. The interface also defines a Reset method that returns the cursor to the start position. No provision is made for the data to be modified.

ICollection

ICollection<el> derives from IEnumerable<el>. It adds a Count property, a Contains method that returns true if some specified value is part of the collection, and a CopyTo method that copies elements from the collection to an array.

ICollection<el> also adds members that allow collections to be modified. First, the IsReadOnly property indicates whether modifications are supported. The Add method adds a single element, Clear deletes all elements, and Remove deletes the first element that matches a specified value. Add, Clear, and Remove all throw if IsReadOnly is false.

Types that implement ICollection<el> can be populated with array initializers:

List<char> oqChs = new List<char> { 'A', 'B', 'C' };
IList

IList<el> derives from ICollection<el>. It adds several methods that support random element access, including an indexer, an Insert method, a RemoveAt method that deletes the element with the specified index, and an IndexOf method that returns the index of the first element that matches the specified value.

Sequence collection classes

ArrayList implements IList with a dynamically-sized array. Because it is not generic, all elements are stored and returned as references to object. This entails frequent casting, and causes value types to be boxed before they are stored.

List<el> implements IList<el>. It provides the features and performance of ArrayList with type safety and little or no boxing. However, methods like Contains and Remove box value types to gain access to object.Equals if the element type does not implement IEquatable<el>. Similarly, methods like Sort box value types if the type does not implement IComparable<el>, and if no IComparer<el> is passed to the method.

LinkedList<el> implements ICollection<el> with a double-linked list.

Map collections

IDictionary

IDictionary<key, val> derives from ICollection<el>, as specialized with KeyValuePair<key, val>. It describes a collection that maps unique keys to single values. It adds a ContainsKey method that returns true if a particular key is part of the collection. Other mapping operations are left unspecified. Types that implement IDictionary<key, val> can be initialized much the way arrays are:

var oqLinks = new Dictionary<int, string>() {
  { 0, "NULL" },
  { 1, "DEFAULT" }
};
ILookup

ILookup<key, el> derives from IEnumerable<el>, as specialized with IGrouping<key, el>. It specifies a collection that maps from unique keys to collections of values. It adds a Contains method that returns true if a particular key is part of the collection, and an indexer that accepts a key and returns an IEnumerable<el> containing the corresponding values.

Map collection classes

Dictionary<key, val> implements IDictionary<key, val> and ICollection<KeyValuePair<key, val>> with a hash table. It maps unique keys to single values. If an IEqualityComparer<el> is passed to the Dictionary<key, val> constructor, that instance is used when comparing keys. If not, the key type's IEquatable<el> implementation is used, or the default comparer, if IEquatable<el> is not implemented.

Hashtable offers similar functionality, but it is not generic, so it suffers from the same type safety and boxing issues that affect ArrayList.

Types serving as hash keys must override object.Equals and object.GetHashCode; otherwise, reference equality and the default object hash code will be used.

Like Dictionary<key, val>, SortedList<key, val> and SortedDictionary<key, val> implement IDictionary<key, val> and ICollection<KeyValuePair<key, val>>. SortedList<key, val> does this with a sorted array, while SortedDictionary<key, val> uses a binary search tree. If an IComparer<el> is specified when constructing either type, that instance is used when comparing keys. If not, the key type's IComparable<el> implementation is used, or the default comparer, if IComparable<el> is not implemented. The classes are similar in most respects, but SortedList<key, val> uses less memory, and supports fast random access to the key and value sets. SortedDictionary<key, val> supports faster insertion and deletion of unsorted elements.

Lookup<key, el> implements ILookup<key, el> and IEnumerable<IGrouping<key, el>>; it maps unique keys to collections of values. Unlike those of other collections, Lookup<key, el> instances are not created and populated on their own, as the class provides no public constructor. Instead, immutable instances are created and returned by IEnumeration.ToLookup.

Text

System.Text.StringBuilder performs complex string operations without creating numerous temporary instances, as string.operator+ would do.

Though it expands its buffer as necessary, the StringBuilder buffer can be sized in advance with Capacity or EnsureCapacity. The size of the stored string is read or set with the Length property.

Individual characters can be read or written with the StringBuilder indexer. Various predefined types are added with the Insert or Append methods. Strings composed with format strings are added with AppendFormat.

Character ranges can be deleted with Remove. Characters or substrings are replaced throughout the string or within a range with Replace.

The stored string can be converted to a string instance with ToString, or to a character array with CopyTo.

Framework miscellanea

The static System.Convert class converts various predefined types to other predefined types.

LINQ

The Language-Integrated Query system (LINQ) supports complex inlined queries with static syntax and type checking. It is compatible with any collection implementing IEnumerable<el> or IQueryable<el>. Common data sources include arrays, List instances, XML data, and remote databases.

Collections implementing only the non-generic IEnumerable cannot be queried, but they can be converted to IEnumerable<el> instances with the Cast and OfType extension methods. Both iterate the sequence, converting all elements deemed compatible by the is operator. Cast throws if an incompatible element is found:

IEnumerable oqObjs = new object[] { 5, 4, "X" };
IEnumerable<int> oqCts = oqObjs.Cast<int>();

while OfType skips such elements:

oqCts = oqObjs.OfType<int>();

Cast and OfType can also be used to convert from one IEnumerable<el> specialization to another.

Queries

LINQ provides an array of standard query operators, these implemented with extension methods in the System.Linq.Enumerable and System.Linq.Queryable classes. Along with the collection instance, many operators accept a delegate or a value used to select or modify individual records. Most operators are implemented as iterators, so they do not execute when the query is created, but rather as it is enumerated by foreach. Those operators that return sequences return them through the same interfaces they accept. Enumerable.Where, for example, could be implemented as:

static IEnumerable<xzEl> Where<xzEl>(this IEnumerable<xzEl>
  aqStars, Func(xzEl, bool) adPred) {

  foreach (xzEl ozEl in aqStars)
    if (adPred(ozEl)) yield return ozEl;
}

LINQ queries can be expressed in several ways. Operators can be invoked from the class that defines them:

string[] oqStars = {
  "Aldebaran", "Canopus", "Altair", "Sirius"
};
IEnumerable<string> oqStarsSel = Enumerable.Where(
  oqStars,
  oq => oq.StartsWith("A")
);

or, as is more common, applied to the collection instance:

oqStarsSel = oqStars.Where(oq => oq.StartsWith("A"));

Because they return a new collection, most operators can be 'chained' when called this way. Chained operators execute in the order in which they are listed:

oqStarsSel = oqStars
  .Where(oq => oq.StartsWith("A"))
  .Select(oq => oq.ToUpper());

Query expressions

Many queries can also be written as query expressions, with a syntax resembling SQL:

oqStarsSel =
  from oq in oqStars
  where oq.StartsWith("A")
  select oq.ToUpper();

Query expressions are converted to a series of operator calls by the compiler. Some queries are more easily written as expressions, but certain operators cannot be used this way.

from

Query expressions begin with one or more from clauses, each of which associates an iteration variable with a sequence to be iterated. The variable represents a specific element at each point within the iteration.

Iteration variable definitions can use variables from preceding lines:

oqStarsSel =
  from oqStar in oqStars
  from oCh in oqStar.ToCharArray()
  select oCh + " in " + oqStar;

Expressions with multiple iteration variables iterate the cross product of the referenced sequences:

string[] oqTags = { "Alpha", "Beta", "Gamma" };
oqStarsSel =
  from oqStar in oqStars
  from oqTag in oqTags
  select oqStar + ' ' + oqTag;

These expressions are implemented with SelectMany:

oqStarsSel = oqStars.SelectMany(
  oqStar => oqTags,
  (oqStar, oqTag) => (oqStar + ' ' + oqTag)
);

let

let stores an expression result in a new variable:

oqStarsSel =
  from oq in oqStars
  let oLen = oq.Length
  where oLen > 6
  select oq + ": " + oLen;

The expression is implemented by projecting the new variable, along with the original iteration variables, into an anonymous type:

oqStarsSel = oqStars
  .Select(oq => new { qOrig = oq, New = oq.Length } )
  .Where(oq => oq.New > 6)
  .Select(oq => oq.qOrig + ": " + oq.New);

orderby

Query output is sorted with orderby and descending:

tPt[] oqPts = { new tPt(0, 0), new tPt(1, 2), new tPt(1, 4) };
IEnumerable<tPt> oqPtsSel =
  from oPt in oqPts
  orderby oPt.X, oPt.Y descending
  select oPt;

Sorting is implemented with OrderBy, OrderByDescending, ThenBy, and ThenByDescending:

oqPtsSel = oqPts
  .OrderBy(o => o.X)
  .ThenByDescending(o => o.Y);

select

Every expression ends with select or group. When into follows one of these, a query continuation is defined. A continuation forwards the output of one query to another query in the same expression:

oqStarsSel =
  from oq in oqStars
  select oq.ToUpper()
  into oqUp
  where oqUp.Contains("US")
  select oqUp;

into serves as a from clause in the continuing query. The new iteration variable replaces those in the first query, which are no longer in scope.

Continuations are implemented by chaining the operators in the two queries:

oqStarsSel = oqStars
  .Select(oq => oq.ToUpper())
  .Where(oq => oq.Contains("US"));

join

The join clause introduces a new iteration variable after join, a new sequence after in, and a filter after on. The filter equates an expression derived from one or more outer variables with one derived from the new inner variable:

oqStarsSel =
  from oqStar in oqStars
  join oqTag in oqTags on oqStar[0] equals oqTag[0]
  select oqStar + ' ' + oqTag;

The filter expressions must be specified in this outer/inner order, because the outer variables are in scope only to the left of equals, and the inner variable only to the right. Join expressions are implemented with the Join operator:

oqStarsSel = oqStars
  .Join(
    oqTags,
    oq => oq[0],
    oq => oq[0],
    (oqStar, oqTag) => oqStar + ' ' + oqTag
  );

Joins are also produced by equating records within a query that contains multiple iteration variables. This filters the cross product just as it would in SQL:

oqStarsSel =
  from oqStar in oqStars
  from oqTag in oqTags
  where oqStar[0] == oqTag[0]
  select oqStar + ' ' + oqTag;

join and join/into provide better performance. When applied to local collections, these operators represent inner sequences with hash tables. In other joins, sequences are enumerated with nested loops.

join/into

Though multidimensional spaces are iterated by joins and other multi-sequence expressions, their output is usually flattened into linear sequences. When into follows a join clause, it defines a group join, which does not flatten the output:

int[] oqLens = { 6, 7, 8, 9 };
IEnumerable<IEnumerable<string>> oqStarsByLen =
  from oLen in oqLens
  join oqStar in oqStars on oLen equals oqStar.Length
  into oq
  select oq;

Instead, it retains the space's two-dimensional structure, returning a sequence of sequences. The outer sequence stores groups, each associated with a single outer iteration value. Each inner sequence stores inner iteration values, all with some common property that relates to the outer value:

foreach (IEnumerable<string> oqStarsOfLen in oqStarsByLen) {
  foreach (string oqStar in oqStarsOfLen)
    Console.WriteLine(oqStar);
  Console.WriteLine("------");
}

Group join expressions are implemented with the GroupJoin operator:

oqStarsByLen = oqLens
  .GroupJoin(
    oqStars,
    o => o,
    oq => oq.Length,
    (oLen, oqStar) => oqStar
  );

Groups are often projected into anonymous types, along with the outer values that determine their content:

var oqStarsByLen =
  from oLen in oqLens
  join oqStar in oqStars on oLen equals oqStar.Length
  into oq
  select new { Len = oLen, qStars = oq };

foreach (var oqStarsOfLen in oqStarsByLen) {
  Console.WriteLine(oqStarsOfLen.Len + ":");
  foreach (string oqStar in oqStarsOfLen.qStars)
    Console.WriteLine(oqStar);
}

group

Group joins return structures that reflect the multidimensionality inherent to all join operations. The group operator, by contrast, converts linear sequences to two-dimensional structures:

IEnumerable<IGrouping<int, string>> oqStarsByLen =
  from oqStar in oqStars
  group oqStar by oqStar.Length;

Instead of returning an IEnumerable of IEnumerable, group returns an IEnumerable of IGrouping. This interface adds a Key property to IEnumerable:

public interface IGrouping<xzKey, xzEl>:
  IEnumerable<xzEl>, IEnumerable {

  xzKey Key { get; }
}

This eliminates the need to project the outer iteration variable into an anonymous type:

foreach (IGrouping<int, string> oqStarsOfLen
  in oqStarsByLen) {

  Console.WriteLine(oqStarsOfLen.Key + ":");
  foreach (string oqStar in oqStarsOfLen)
    Console.WriteLine(oqStar);
}

Grouping expressions are implemented with GroupBy:

oqStarsByLen = oqStars.GroupBy(oq => oq.Length);

Query continuations are often applied to group operations to manipulate the returned groups:

IEnumerable<IGrouping<int, string>> oqStarsByLen =
  from oqStar in oqStars
  group oqStar by oqStar.Length
  into oq
  where oq.Count() > 1
  select oq;

Query operators

Query operators are used to implement query expressions, but they can also be used on their own. Operators returning a single element or value are executed immediately, as are conversion operators like ToArray. Other operators are executed only as elements are enumerated by foreach.

Many operators are overloaded to pass the record index to the specified method as well as the record itself:

oqStarsSel = oqStars
  .Select((oqStar, oIdx) =>
    (oIdx.ToString() + ' ' + oqStar.ToUpper())
  );

Generation operators

Empty returns an empty sequence.

Repeat returns a sequence containing a single value repeated some number of times. Range returns a sequence of contiguous integers.

Set manipulation operators

Concat returns a new sequence containing the elements in the collection upon which it was invoked, plus those in a second sequence.

Union returns all elements appearing in either sequence, with duplicates removed. Intersect returns one instance of every element appearing in both sequences. Except returns all elements in the first sequence that do not appear in the second. Union, Intersect, and Except define overloads accepting an IEqualityComparer<el>.

Projection operators

Projection operators transform and project the elements from one or more sequences into a new sequence. Select projects a single sequence:

oqStarsSel = oqStars.Select(oqStar => oqStar.ToUpper());

SelectMany projects two sequences:

oqStarsSel = oqStars
  .SelectMany(
    oqStar => oqTags,
    (oqStar, oqTag) => (oqStar + ' ' + oqTag)
  );

As the outer sequence is iterated, each element is projected into an inner sequence, which is then itself iterated. The outer and inner elements are transformed together into output elements. Other SelectMany overloads flatten the sequences without transforming the pairs.

Filter operators

Where returns elements for which the specified predicate is true:

oqStarsSel = oqStars.Where(oq => oq.Contains("us"));

Take returns a number of elements from the beginning of the sequence. Skip returns all elements except some number from the beginning.

TakeWhile returns all elements until the predicate returns false, after which the iteration ends. SkipWhile ignores elements until the predicate returns true, then it returns the current element and those that follow it.

Distinct returns one instance of each element, with no duplicates. One overload accepts an IEqualityComparer<el> to be used when identifying duplicates.

Order operators

Reverse returns the sequence in reverse order.

OrderBy and OrderByDescending return a sorted copy of the sequence. ThenBy and ThenByDescending refine the sort:

oqPtsSel = oqPts
  .OrderBy(o => o.X)
  .ThenByDescending(o => o.Y);

All OrderBy and ThenBy operators define overloads specifying an IComparer<el> to be used when sorting.

Join operators

Join transforms elements from the invoking sequence and a new inner sequence to create join key pairs. When pairs match, the associated values are transformed into output elements:

IEnumerable<string> oqStarsSel = oqStars
  .Join(
    oqTags,
    oq => oq[0],
    oq => oq[0],
    (oqStar, oqTag) => oqStar + ' ' + oqTag
  );

GroupJoin functions as Join does, but without flattening its output:

IEnumerable<IEnumerable<string>> oqStarsByLen
  = oqLens.GroupJoin(
    oqStars,
    o => o,
    oq => oq.Length,
    (oLen, oqStar) => oqStar
  );

Both Join and GroupJoin define overloads specifying an IEqualityComparer<el> to be used when comparing keys.

Zip iterates two sequences simultaneously, allowing elements with the same index in either sequence to be transformed together. The iteration ends after passing the last element in either sequence.

Group operators

GroupBy partitions the sequence into groups. Some overloads return an IEnumerable<el> of IGrouping<key, el> sequences, each containing elements found to have the same key, as defined by the specified key function:

IEnumerable<IGrouping<int, string>> oqStarsByLen
  = oqStars.GroupBy(oq => oq.Length);

IGrouping<key, el> derives from IEnumerable<el>, to which it adds the Key property:

foreach (IGrouping<int, string> oqStarsLen
  in oqStarsByLen) {

  Console.WriteLine(oqStarsLen.Key + ":");
  foreach (string oqStar in oqStarsLen)
    Console.WriteLine(oqStar);
}

Other overloads transform elements after they are grouped:

oqStarsByLen = oqStars.GroupBy(
  oq => oq.Length,
  oq => "(" + oq + ")"
);

Some overloads do not return IGrouping<key, el> sequences. They transform each group and its key into a single value, which is then returned as part of an IEnumerable<el>:

IEnumerable<string> oqCtsByLen = oqStars
  .GroupBy(
    oq => oq.Length,
    (oLen, oqStarsOfLen) =>
      (oLen.ToString() + ": " + oqStarsOfLen.Count())
  );

Others specify an IEqualityComparer<el> to be used when comparing element keys. Other overloads implement several of these variations at once.

Conversion operators

ToArray and ToList convert the sequence to an array or a list.

ToDictionary and ToLookup convert the sequence to a Dictionary<key, val> or Lookup<key, el> instance. Both accept functions that transform sequence elements into key values. Both define overloads that transform the new collection members, or that accept an IEqualityComparer<el> to be used when comparing element keys.

AsEnumerable downcasts the sequence to IEnumerable<el>. AsQueryable converts the sequence to IQueryable<el>.

Qualification operators

Contains returns true if the sequence includes the specified element. SequenceEqual returns true if the sequence matches another sequence. Both define overloads specifying an IEqualityComparer<el>.

Any returns true if the sequence is non-empty. If a predicate is specified, it returns true if the predicate returns true for any element. All returns true if the predicate returns true for all elements.

Element extraction operators

First returns the first element in the sequence. If a predicate is specified, it returns the first for which the predicate returns true. Last returns the last element, or the last for which a predicate returns true. ElementAt returns the element at the specified index. First, Last, and ElementAt throw if the specified element cannot be found. FirstOrDefault, LastOrDefault, and ElementAtOrDefault return a default value instead.

Single returns the only element in the sequence, throwing if the sequence contains more or less than one. If a predicate is specified, Single returns the only element for which that predicate returns true, throwing if it returns true for more or less than one. SingleOrDefault returns the default value if no element is found, the element if one is found, and throws if more than one is found. As before, if a predicate is specified, only elements for which the predicate returns true are considered.

DefaultIfEmpty returns the entire sequence if the sequence is non-empty. If it is empty, it returns a sequence containing the default value, or a custom default passed to one of the overloads.

Aggregation operators

Count returns the element count, or the number of elements for which a predicate returns true. LongCount does the same, but it returns an Int64 instead of an Int32.

Min, Max, Sum, and Average return values that summarize the sequence:

int oLenMin = oqStars.Min(oq => oq.Length);

Aggregate performs a custom aggregation on the sequence:

string oq = oqStars.Aggregate(
  (oqAggr, oqStar) => oqAggr + ' ' + oqStar
);

Each element is passed to the specified function along with the value that was returned during the last call. Other overloads specify an initial value, or a second function that transforms the final value.

Miscellanea

Identifiers

Identifiers are composed of one or more Unicode characters. They must begin with a letter or underscore. To use identifiers that would otherwise conflict with C# keywords, prefix them with @:

int @Int32 = 0;
Console.Write(@Int32);

This prefix does not become part of the identifier; it merely marks it as an identifier. It can therefore be omitted later if doing so would not cause ambiguity:

int @oInt = 1;
Console.Write(oInt);

A text representation of any identifier can be obtained with the nameof operator:

void eAdd_Line(char aCd) {
  string oqLine = String.Format("{0}: {1}",
    nameof(aCd), aCd);
  ...

object

All types derive ultimately from System.Object, aliased with object. All types can be upcast to object, but, because it is a reference type, value types must be boxed first. object implements these methods:

public Type GetType();
public virtual string ToString();
public static bool ReferenceEquals(object, object);
public static bool Equals(object, object);
public virtual bool Equals(object);
public virtual int GetHashCode();

protected object MemberwiseClone();
protected virtual void Finalize();

Because all types derive from object, even non-null literals provide these methods:

Type oq = 0.GetType();

GetType

object.GetType returns the System.Type instance representing the dynamic type of the referenced instance:

Type oq = oPt.GetType();

typeof obtains the Type instance from a type name, rather than an instance:

oq = typeof(tPt);

GetHashCode

object.GetHashCode is meant to return hash codes for use with collections like Hashtable and Dictionary. The base implementation is not considered to be reliable for user types, and such types should override GetHashCode if they are to serve as hash table keys. One common implementation adds subsidiary hashes into a value that is continuously compounded by a prime number:

struct Route {
  public int Group;
  public string Region;
  ...

  public override int GetHashCode() {
    // This technique is recommended here:
    //
    //   https://stackoverflow.com/a/263416/3728155
    //
    unchecked {
      int o = 17;
      o = (o * 23) + Group.GetHashCode();
      o = (o * 23) + Region.GetHashCode();
      ...
      return o;
    }
  }

If two instances are considered equal by their Equals implementation, their GetHashCode override must return the same value for both. It is not necessary that two instances returning the same hash be equal, but it is desirable. For performance reasons, it is also desirable that hash values be evenly distributed throughout their range.

ToString

For predefined types, object.ToString returns the content of the referenced instance converted to a string. For user types, the base implementation returns the name of the type, qualified by any containing namespaces.

Attributes

Attributes attach metadata to types, members, and other code elements. They are created by subclassing System.Attribute:

class LinkAttribute: Attribute {
  ...

They are applied by placing the attribute name in square brackets before the target element. If the name ends with 'Attribute', that part of the name can be omitted:

[Link] public void Exec() {
  ...

Attributes targeting an assembly specify their target with the assembly keyword:

[assembly: Link]

Multiple attributes can be applied to a single element by listing them in separate bracket sets:

[Link][Part(10)] public void Wait() {
  ...

or by comma-delimiting them in the same set:

[Link, Part(10)] public void Wait() {
  ...

Starting with C# 7.3, an attribute can be applied to the variable backing an automatic property by prefixing its name with field:

[field: CkSecAttribute]
public int CtRef { get; set; }

Attribute parameters

Data can be passed to attributes with positional parameters or named parameters.

Positional parameters correspond to constructor parameters in the attribute class:

class PartAttribute: Attribute {
  public PartAttribute(byte aID) {
    ID = aID;
  }

  public byte ID;
  public string qName;
  public bool Open = false;
}

Positional parameters, if any, must precede named parameters, and they must match the signature of one of the attribute class constructors:

[Part(0)] public int Idx;

If no positional parameters are specified when the attribute is applied, the attribute class must provide a parameterless constructor.

Named parameters correspond to variables or properties in the attribute class. They can be specified in any order, or not at all:

[Part(1, Open = true, qName = "Main")] public int Ct;

Attribute arguments must be constant expressions, typeof expressions, or array expressions that contain such values.

AttributeUsage

To control the way an attribute class is applied, add an AttributeUsage attribute to its definition. The AttributeTargets values passed to the AttributeUsage constructor determine the specific code elements to which the attribute can be applied:

[AttributeUsage(
  AttributeTargets.Field | AttributeTargets.Property)]
class TagAttribute: Attribute {
  ...

AttributeUsage can allow or disallow multiple applications of the same attribute to a given element, and it can enable or disable the inheritance of an attribute from one class to another.

Reading attributes

At run time, attributes can be read from Type or MemberInfo instances with the GetCustomAttributes overloads defined in those classes. Attribute collections can be read from other elements by passing element metadata to the static Attribute.GetCustomAttributes methods. Specific attributes can be queried with Attribute.GetCustomAttribute.

Preprocessor directives

C# is not preprocessed per se, but many directives work as they do in C++.

Preprocessor directives cannot share lines with other instructions.

Preprocessor symbols

#define creates a symbol for use by other directives. No value can be associated with the symbol, and it cannot be read except by other directives. #undef clears a defined symbol.

Conditional directives

#if, #else, #elif, and #endif cause lines to be conditionally compiled. No 'ifndef' directive is provided, but a symbol can be checked for non-existence by prefixing it with an exclamation point. Symbols can also be combined with && or ||:

#if !mSilent || mDebug
Console.WriteLine("Done");
#endif

#warning and #error

If the compiler encounters a #warning directive, the string following the directive is displayed in the output window as a warning. If the compiler encounters an #error directive, compilation is aborted, and the string is displayed as an error.

#region and #endregion

#region and #endregion define outlining regions, which can be collapsed or expanded within Visual Studio by clicking in the margin.

#line

#line overrides the compiler's record of the line number:

#line 10

or the line number and file name:

#line 10 "Test.h"

The default option resets the line number and file name to their expected values:

#line default

The hidden option causes all code up to the next #line directive to be skipped by the debugger. The hidden code will be executed, but the debugger will not stop within it:

#line hidden
Exec();
#line default

#pragma

#pragma directives forward other instructions to the compiler. In particular, warnings can be suppressed or restored with the warning disable and warning restore options:

class tqMsg {
  #pragma warning disable 0649
  public string Text;
  #pragma warning restore 0649
  ...

The Main method

The entry point for every C# program is a static method called Main. The method can accept a string array or no parameters at all. It can return an int or void:

public static int Main() {
  return 0;
}

Main can be defined within any class. It is commonly declared public, but it will work as an entry point no matter what its access level.

Ordinarily, the compiler will not allow more than one method to meet the entry point criteria. If more than one must be defined, the desired entry point can be specified with the /main compiler switch.

The string array parameter, if defined, stores any command line arguments passed to the executable. Unlike C++, it does not store the executable name:

public static void Main(string[] aqArgs) {
  foreach (string oq in aqArgs)
    Console.WriteLine(oq);
}

Starting with C# 7.1, Main can be declared async:

static async Task Main() {
  await cPrep();
  await cExec();
}

If an integer result is required, it can return Task<int>.

Assemblies

An assembly can contain an application, a number of libraries, or both.

Command-line compilation

C# programs can be built from the command line by invoking the compiler directly. To open a command line with the relevant environment variables already set, select Developer Command Prompt from the Windows Start menu, or select Tools / Visual Studio Command Prompt from the menu in older versions of Visual Studio.

To build the problem, pass one or more C# files to cs.exe:

csc BuildRsc.cs LibRsc.cs

Files can also be specified with global characters:

csc *.cs

By default, the executable name derives from the name of the source file defining Main. The name can be specified explicitly with the /out switch, which must be passed before the source files:

csc /out:Build.exe *.cs

Preprocessor symbols can be defined with the /define switch:

csc *.cs /define:DEBUG

Sources

C# 6.0 Pocket Reference
Joseph Albahari, Ben Albahari
2015, O'Reilly Media

Professional C# 2005
Christian Nagel, Bill Evjen, Jay Glynn, Morgan Skinner, Karli Watson, Allen Jones
2006, Wiley Publishing

Effective C#, Third Edition
Bill Wagner
2017, Addison-Wesley

C# in Depth
Gotchas in dynamic typing
Retrieved May 2016

Stack Overflow
What is the best algorithm for an overridden System.Object.GetHashCode?
Proper use of the IDisposable interface
Retrieved March 2018 - February 2019

Microsoft Docs
Implementing a Dispose method
C# tuple types
Pattern Matching
in parameter modifier
Ref struct types
Unmanaged types
What's new in C# 7.0
What's new in C# 7.1
What's new in C# 7.2
What's new in C# 7.3
Retrieved February 2019 - January 2020