C# Notes
Compiled by Jeremy Kelly
www.anthemion.org
Printed UNKNOWN
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
- Booleans
- Integers
- Overflow checking
- Floating point numbers
- Special floating point values
- Characters
- Enumerations
- Boxing
- Reference types
- Arrays
- Rectangular arrays
- Jagged arrays
- Creating arrays
- Copying arrays
- Reading arrays
- Modifying arrays
- string
- Verbatim strings
- Interpolated strings
- Tuples
- Tuple class
- ValueTuple structure
- Deconstructing tuples
- Deconstructing other types
- Classes and structures
- static
- using static
- Constructors
- Finalizers
- Properties
- Indexers
- Methods
- Parameters
- ref
- in
- out
- params
- Optional parameters
- Named arguments
- Overloads
- Operator overloads
- Extension methods
- extern
- Access levels
- public
- internal
- protected
- protected internal
- private
- private protected
- Partial types
- Partial methods
- Anonymous types
- Instance equality
- ReferenceEquals
- static Equals
- non-static Equals
- Equality operator
- Equality with value types
- Inheritance
- base
- new
- virtual and override
- abstract
- sealed
- Interfaces
- Explicit interface implementation
- Interface reimplementation
- Generics
- Generic classes and structures
- Generic interfaces
- Generic methods
- Generic delegates
- Generic constraints
- class and struct constraints
- Base class constraints
- Interface constraints
- Parameterless constructor constraints
- unmanaged constraints
- Type argument variance
- default
- Type conversion
- Casts
- as
- is
- Conversion operators
- true and false operators
- Namespaces
- using directives
- using aliases
- Variables
- Variable declaration
- var
- dynamic
- Limitations of dynamic
- Variable initialization
- const
- readonly
- Null values
- Nullable types
- Operator lifting
- Null-coalescing operator
- Null-conditional operators
- Statements
- goto
- switch
- foreach
- Iterators
- break
- continue
- Delegates
- Delegate compatibility
- Multicasting
- Events
- Lambda expressions
- Outer variables
- Anonymous methods
- Multithreading
- Creating threads
- Thread class
- Thread pooling
- volatile
- lock
- Asynchronous programming
- Tasks
- async and await
- Exceptions
- throw
- catch
- Exception filters
- finally
- Unsafe code
- Pointer types
- unsafe
- fixed
- fixed buffers
- stackalloc
- Resource management
- using statements
- IDisposable
- Framework
- Collections
- Sequence collections
- IEnumerable and IEnumerator
- ICollection
- IList
- Sequence collection classes
- Map collections
- IDictionary
- ILookup
- Map collection classes
- Text
- Framework miscellanea
- LINQ
- Queries
- Query expressions
- from
- let
- orderby
- select
- join
- join/into
- group
- Query operators
- Generation operators
- Set manipulation operators
- Projection operators
- Filter operators
- Order operators
- Join operators
- Group operators
- Conversion operators
- Qualification operators
- Element extraction operators
- Aggregation operators
- Miscellanea
- Identifiers
- object
- GetType
- GetHashCode
- ToString
- Attributes
- Attribute parameters
- AttributeUsage
- Reading attributes
- Preprocessor directives
- Preprocessor symbols
- Conditional directives
- #warning and #error
- #region and #endregion
- #line
- #pragma
- The Main method
- Assemblies
- Command-line compilation
- Sources
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
orout
; - 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
andobject
; - 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 aMethodBase
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 anIDictionary
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