Personal C# notes
Compiled by Jeremy Neal Kelly
www.anthemion.org
February 15, 2010
These are my personal C# notes, covering C# 3.0. I have published them primarily for my own convenience, but others are welcome to distribute them, in whole or in part, so long as proper citation is made.
These notes do not provide an exhaustive description of the language; concepts that seem obvious to me are generally not mentioned. Nor is everything here necessarily useful. The notes are not drawn from the C# standard, but from various secondary sources listed at the end. I know C++ well, but I am not an experienced C# developer, so there may be mistakes here, and my terminology may vary from the standard. If you find a mistake, please let me know.
Value types are stored on the stack. 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, all predefined numeric types, and all enumerations.
| Type | Alias | Size |
| Boolean | bool | 1 byte |
| 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 |
Integer literals prefixed with 0x specify hexadecimal numbers:
const UInt16 oLenMax = 0x1000;
Arithmetic operators are not defined for one or two-byte integers; such operands, whether signed or unsigned, are automatically promoted to Int32.
Integer expressions evaluated by the compiler are checked for overflow at compile time. Run-time operations are not checked, however, unless marked with the checked operator, which may be applied to expressions:
o = checked (o - 1);
or blocks:
checked {
--o;
...
If overflow occurs in code thus protected, OverflowException is thrown. Checking can be enabled for the entire program with the /checked+ compiler switch.
Both compile-time and run-time checking can be disabled within expressions or blocks with the unchecked operator:
checked {
UInt32 o = 0;
o = unchecked (o - 1);
}
| 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 |
Unless otherwise specified, real number literals are treated as doubles.
Real number literals can be specified with scientific notation, the exponent being marked with E or e:
const float oTol = 1E-3;
decimal is not a primitive type, so its performance cannot match that of float or double.
A number of special values are defined for float and double, but not decimal; these are PositiveInfinity, NegativeInfinity, and NaN.
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 substracting negative infinity from negative infinity produces NaN. This value is considered to be unequal to every 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;
| 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 \x and the value in four or fewer hex digits:
const char oLF = '\xA';
Enumerations are implemented 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 set to zero, and all others have values one greater than their precedent. Values can be listed in any order, and can even 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 and other enumerations with casts. The literal 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
}
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 may be applied to bitfield-like enumerations. It causes the ToString method to return the names of the members included in some value, rather than the numeric value itself:
[Flags]
enum tOpt {
None = 0,
Ver = 1,
Sync = 2,
Log = 4
}
Boxing allocates memory on the managed heap, where reference types are stored, and copies the content of a value type there, allowing that data to be used as reference types are. It is performed implicitly when a value type:
struct tPt {
public Int32 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 a copy is made, changes to the original value type 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 type instances are stored in the managed heap, though references themselves are stored on the stack. Reference type instances consume the memory used by their data and padding, plus an additional quantity for overhead, typically twelve bytes. References consume four or eight bytes.
Reference assignment copies instance addresses rather than content. Unlike value types, references can be set to null.
All classes are reference types, as are interfaces, delegates, arrays, and string instances.
Though it is not evident from their syntax, all arrays derive from a common class, System.Array. This class implements IList and ICloneable.
The length of an array instance is determined when that instance is created:
string[] oqDirs = new string[4];
References can be set with new instances, and the content of existing instances can be changed, but instances can never be resized.
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 is required to match the length of the initializer. If no initializer is provided, the elements are default-initialized.
Bounds checking is performed during every array access, with IndexOutOfRangeException being thrown if the check fails.
Value type array elements are allocated 'in place' within the array; they are not boxed.
Rectangular arrays are unitary, multidimensional arrays with consistent lengths throughout a given dimension:
char[,] oqCon = new char[80, 25];
Array.Length returns the element count for the entire array, not the length of a particular dimension. Dimension lengths are retrieved by passing their index to Array.GetLength:
for (Int16 oX = 0; oX < oqCon.GetLength(0); ++oX)
for (Int16 oY = 0; oY < oqCon.GetLength(1); ++oY)
oqCon[oX, oY] = ' ';
Rectangular arrays can be set 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 are not unitary; they are arrays of arrays. Component arrays may 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, Array.Length, when applied to jagged arrays, returns the length of the leftmost dimension.
Strings are usually represented with System.String, aliased with string. Instances of this class are immutable: they cannot be modified, only replaced. Complex string operations are performed more efficiently by System.Text.StringBuilder.
Though string is a reference type, its equality and inequality operators compare instance content, not addresses.
The string concatenation operator automatically converts non-string operands to strings with ToString:
string oqText = 9 + " coins";
string implements IEnumerable<char>, IComparable<string>, IEquatable<string>, IConvertible, and ICloneable.
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 indexer allows individual characters to be read or modified.
The static Compare method compares strings or substrings, with options for case-insensitive comparisons, or other, culture-specific variations.
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 character array elements.
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 the elements of 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 return values constructed from 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.
Escape sequence processing can be disabled by prefixing string literals with @:
string oqPath = @"C:\Temp\";
Verbatim literals 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 literals by escaping each with another double quote:
string oqText = @"The goblin says ""Spare a coin?""";
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 real numbers, however:
float o = 123456789;
Single-parameter constructors do not implement implicit conversions as they do in C++.
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.
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, however, 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.
The is operator returns true if the specified cast can be performed without an exception:
if (oqOut is tqOutFile) oqOutFile = (tqOutFile)oqOut;
Conversions implicit or explicit are defined with conversion operators, which can convert to or from the types defining them. The conversion operand defines the source type; the operator name, 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;
}
Explicit conversions are performed with casts:
tSpan oSpan = (tSpan)10;
Implicit conversions are performed automatically:
Int32 oWth = oSpan;
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:
public void Plot(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. Starting with C# 2.0, however, nullable bool types provide a simpler solution to this problem.
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 existing namespaces just as they are added to new ones:
namespace nMath {
namespace nTrig {
...
Nested namespaces can be defined from outside the parent namespaces, even when one or more parents have yet to be defined:
namespace nMath.nRnd.nMersenne {
class tqGen {
public Int32 zInt32() {
...
Any namespaces or types not explicitly defined within a namespace become part of the global namespace. This namespace can be used to specify such elements, but it must be suffixed with ::, not .:
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 {
...
A using directive allows the members of some namespace to be referenced from a different namespace without fully qualifying them each time, though if the same names are found within several used namespaces, they must continue to be qualified to avoid ambiguity. If used within the global namespace, the directive's effect is limited to the containing unit. Directives must be placed at the beginning of the namespace within which they are used, or at the top of the unit if they are in the global namespace:
namespace tPhys {
using nMath;
struct tProj {
public tPt Pos;
...
using directives can precede the definitions of the namespaces they reference:
using nMath;
namespace nMath {
...
using can also be used to alias namespaces:
using nRndMersenne = nMath.nRnd.nMersenne;
or namespace members:
using fTraitRnd = nMath.nRnd.nTrait.fTrait;
As with using directives, alias definitions must be placed at the beginning of the namespace or unit within which they apply. They are allowed to reference namespaces or namespace members that have yet to be defined.
If several local or class variables share the same type and qualifiers, they can be declared together:
Int32 oCtMin = 1, oCt;
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.
Identifiers are composed of one or more Unicode characters beginning 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);
No local variable may 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.
Like local variables, class variables can be initialized where they are declared:
Int32 eCt = 3;
Structure variables, however, 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.
One or more variables or properties within a class or structure:
class tqSumm {
public tqSumm() {}
public tqSumm(Int32 aAct) { Act = aAct; }
public Int32 Pend, Act;
public Int32 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 assigned values 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 it can be performed only at construction time, this type of assignment does not constitute initialization per se; readonly variables cannot be assigned this way, and declaration and constructor initializations are still performed, though their work is superceded by the assignment.
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.
Local or class variables declared const cannot be modified after they are initialized. Their values are calculated at run time, so they must be initialized with constant expressions:
const Int32 oTicksPerMin = 60 * 1000;
Class variables declared readonly cannot be modified after they are initialized. If their default value is not to persist, they must be initialized where they are declared:
readonly Int32 eCtMax = 10;
or within the body of a constructor:
public tqMgr() {
eCtMax = 10;
}
Local variables may not 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 variables declared const, readonly variables can be initialized with non-constant expressions.
The default operator returns the default value of the specified type. It can be applied to predefined types or user types:
tPt<byte> oPt = default(tPt<byte>);
Parameters declared with ref are passed by reference:
public static void sSwap(ref Int32 ar0, ref Int32 ar1) {
Int32 o0 = ar0;
ar0 = ar1;
ar1 = o0;
}
Formal parameters passed by reference alias actual parameters rather than copying them, which allows actual parameters to be modified from within the function. Simply passing a reference does not itself constitute passing by reference.
To clarify the effect of passing by reference, actual ref parameters must also be marked with ref:
sSwap(ref oX, ref oY);
Variables must be assigned before being passed as ref parameters.
Parameters declared with out are also passed by reference, but variables need not be assigned before being passed to them. As with ref, actual out parameters must be marked with out:
Int32 oTop; Int32 oHgt = 10; Reset(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 within the function, even if they were assigned before being passed:
public void Reset(out Int32 arTop, out Int32 arHgt) {
...
arTop = 0;
arHgt = 0;
}
If some function's last parameter is an array, and if it is declared params:
public void Out(string aqName, params Int32[] aqVals) {
...
any number of actual parameters of the array type may be passed in its place:
Out("Rates", 2, 4, 8);
An array of the appropriate type may also be passed in its place:
Int32[] oqRates = { 2, 4, 8 };
Out("Rates", oqRates);
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 values must be strings, enumerations, or integral values, including bool and char.
Most cases must end with an explicit jump; the program may not 'fall through' to another case unless the case is empty. Control may be passed to other cases, including the default case, with goto:
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.
Any type implementing IEnumerable or IEnumerable<> can be iterated with foreach:
foreach (char oCh in "iron") Console.Write((char)(oCh + 1));
IEnumerator and IEnumerator<> specify forward-only cursors used to enumerate a sequence of values. Enumerators are types that implement these interfaces or define the Current and MoveNext members specified therein. They are used as visitor instances within foreach implementations.
Enumerable types are those that implement IEnumerable or IEnumerable<>, or that define a GetEnumerator method that returns an enumerator, as IEnumerable specifies. The enumerable's ability to generate enumerators is what allows it to be used with foreach.
Iterators are methods, properties, or indexers that contain yield statements and return one of the IEnumerator or IEnumerable interfaces. Iterators returning IEnumerator can be used to implement IEnumerable:
class tqSet: IEnumerable {
public IEnumerator GetEnumerator() {
yield return 10;
yield return 18;
}
}
which allows the implementing type to be passed to foreach:
tqSet oqSet = new tqSet(); foreach (Int32 o in oqSet) ...
Iterators returning IEnumerable:
IEnumerable<string> eEls(char aCh) {
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 implementing IEnumerable<> and IEnumerator<>:
foreach (string oq in eEls('X'))
...
Each invocation of such an iterator corresponds with a MoveNext call in the backing enumerator.
State within iterators is extended to the lifetime of the active foreach operation, which continues until the end of the iterator block is reached, or until yield break is executed.
break jumps out of the enclosing for, while, do while, or switch statement. Only one such statement is terminated.
continue jumps to the end of the enclosing for, while, or do while statement, not the beginning. The distinction is important within do while loops, where continue causes the loop condition to be retested.
Structures differ from classes in the following ways:
Static constructors are executed once per class or structure, before any other static method, and before any instance is created. A class or structure may define only one static constructor, and it may have no access modifier or parameters:
struct tRoll {
static tRoll() {
...
The relative execution order of static constructors in different types is undefined.
Classes containing only static members can be declared static:
static class tqMetr {
public static float HgtMax;
...
Though they can define static members, structures cannot be declared static.
If no constructor is explicitly defined for some class, the compiler will create a default constructor for it. If a constructor is explicitly defined, no default will be created. Structures, by contrast, 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 may invoke another constructor in the same class or structure with this:
class tqMgr {
public tqMgr(Int32 aCt) { Ct = aCt; }
public tqMgr(Int32 aCt, Int32 aCtMax): this(aCt) {
CtMax = aCtMax;
}
...
The constructor thus invoked is executed first.
A constructor in the base class can be invoked with base:
class tqMgrStd: tqMgr {
public tqMgrStd(Int32 aCt): base(aCt) { }
...
The finalizer for some class is executed just before the garbage collecter deallocates an instance of that class:
class tqTree {
~tqTree() { ... }
...
Because it is triggered by garbage collection, exact finalization timing cannot be known.
Child class finalizers are invoked before those of parent classes.
Defining a finalizer is equivalent to overriding object.Finalize, which cannot be overridden any other way. Structures cannot define finalizers.
Properties must define a get accessor, a set accessor, or both. Within a set accessor, the incoming operand is represented by value:
Int32 eWth;
public Int32 Wth {
get { return eWth; }
set { Right = value - Left; }
}
Automatic properties do not require the definition of a backing variable:
public Int32 Ttl { get; set; }
Properties cannot be declared readonly or const. They can be made read-only or write-only, however, by omitting the relevant accessor or changing its access level.
An access modifier can be applied to one of the accessors, so long as it is made less accessible than the property as a whole:
public Int32 Ct { get; protected set; }
Indexers are defined as properties are, but they are unnamed, and they accept one or more parameters of any type:
class tqMgr {
public Int32 this[string aqKey, char aCd] {
get { return tqStore.sRest(aqKey, aCd); }
set { tqStore.sSave(aqKey, aCd, value); }
}
}
Their use resembles that of an array:
tqMgr oqMgr = new tqMgr(); oqMgr["alpha", 'a'] = 100;
Indexers may be overloaded as methods are.
Overload calls are resolved at compile time, so static typing is used when matching actual parameters 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(Int32 aCt) { ... }
public void Exec(ref Int32 arCt) { ... }
public void Ck(Int32 aIdx) { ... }
public void Ck(out Int32 arIdx) { ... }
}
ref parameters are not distinguished from out parameters, however.
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, Int32 a) {
return new tPt(aPt.X * a, aPt.Y * a);
}
public static tPt operator*(Int32 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, however, 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<> be implemented.
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 aPt0, tPt aPt1) {
Int32 o = Math.Abs(aPt0.X - aPt1.X);
if (o > 1) return false;
o = Math.Abs(aPt0.Y - aPt1.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 privilages with respect to that type; in particular, they cannot access its non-public members.
Extension methods can target interfaces 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.
Methods declared extern are implemented externally, in a language other than C#. Such declarations may be used to call functions imported from unmanaged libraries:
[DllImport("Port.dll")]
public static extern byte Read(Int32 aIdx);
Subclasses can be made less accessible than parent classes, but not more accessible. Access levels cannot be changed when overriding methods.
public members are accessible anywhere. This is the default level for enumeration and interface members.
internal members are accessible within the local assembly, and within friends of the local assembly. This is the default level for non-nested types.
protected members are accessible throughout the containing class or structure and its subclasses. This includes all parts of any types nested within the class or structure and its subclasses.
protected internal members are accessible everywhere protected or internal members are accessible.
private members are accessible throughout the containing class or structure, including all parts of any types nested therein. This is the default level for class and structure members.
Classes, structures, and interfaces declared partial can be defined with multiple blocks, even blocks in different files:
partial struct tLvl {
string Name;
}
...
partial struct tLvl {
tPt Size;
}
All blocks must be declared partial, however, 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 thusly. A base class may 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 classes and structures may declare partial methods, which can be defined in one block and implemented in another:
partial class tqMgr {
partial void Exec(string aqText);
}
partial class tqMgr {
partial void Exec(string aqText) {
...
}
}
Both declaration and 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 declared but not defined are removed from the class, and calls to them are discarded.
Anonymous types group data fields into a class without explicitly defining that class. They are created and initialized simultaneously:
Int32 oWt = 18;
var oqMark = new { ID = 100, Pos = new tPt(10, 25), Wt = oWt };
The names and types in the initializer expression define a set of read-only properties in the unnamed type:
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 adopted by the property:
Console.WriteLine(qLink.Next);
Though they are known as anonymous 'types', only classes can be created this way. Anonymous types directly subclass object, and cannot be cast to any other type. Anonymous types defined with the same members in the same order are treated as the same type. Anonymous types have method scope, so they cannot be passed from a method without first being cast to object.
Interfaces can define methods, properties, indexers, and 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 gains all members of that parent. Interfaces can inherit from multiple parents.
An instance can be implicitly cast to any interface it implements:
tqiRev oqiRev = new tqiRev(); fi oqi = oqiRev;
If the same name is used by members of different interfaces:
interface fi {
bool Ck { get; }
...
interface fHand {
bool Ck(Int32 aIdx);
...
and if those interfaces are implemented by the same type, one or more members must be explicitly implemented:
class tqiHand: fi, fHand {
public bool Ck {
get { ... }
}
bool fHand.Ck(Int32 aIdx) { ... }
...
Explicitly implemented members cannot bear access modifiers; their public accessibility is implicit in their role as implementations. They also, unlike implicitly implemented members, cannot be declared new or virtual.
Not all members with conflicting names need be implemented explicitly; one can be implemented implicitly without creating ambiguity. Interface members can also be implemented explicitly when no conflict exists, and this is sometimes done to set certain members apart.
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);
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 {
Int32 Ct();
}
class tqFilt: fFilt {
Int32 fFilt.Ct() { ... }
}
// Reimplement fFilt:
class tqFiltAll: tqFilt, fFilt {
Int32 fFilt.Ct() { ... }
}
This does not provide polymorphic behavior when the method is invoked through an instance type. When it is invoked through the interface, however, the most recent interface implementation is used:
tqFilt oqFilt = new tqFiltAll(); Int32 oCt = ((fFilt)oqFilt).Ct();
A class may specify at most one parent class.
Structures inherit implicitly from ValueType. They cannot inherit or be inherited, but they can implement interfaces.
Parent class implementations, whether hidden or overridden, can be accessed with the base keyword:
class tqQue {
public virtual void vAdd() {
...
class tqQueDbl: tqQue {
public override void vAdd() {
base.vAdd();
...
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 Int32 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 class:
tqMgr oqMgr = new tqMgr { Cd = 10 };
tqMgrStd oqMgrStd = new tqMgrStd { Cd = false };
(oqMgrStd as tqMgr).Cd = 20;
The compiler warning will not appear, however, if the new member is declared new:
class tqMgrStd: tqMgr {
new public bool Cd;
}
Since structures do not support inheritance, they may not define virtual members.
A member's access level cannot be changed when it is overridden.
Properties, indexers, and events can be declared virtual, just as methods can:
class tqMgr {
public virtual Int32 vTop { get; set; }
}
If only one accessor is specified in a property or indexer override, the other will function as before. Properties and indexers cannot be made read-only or write-only by omitting an accessor from the override:
class tqMgrCk: tqMgr {
Int32 eCtRead;
public override Int32 vTop {
get {
++eCtRead;
return base.vTop;
}
}
}
Both accessors must be specified when an event is overridden.
Classes declared abstract cannot be instantiated. They can define members 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();
}
Overrides declared sealed cannot be further overridden by any child class:
class tqMgrStd: tqMgr {
public sealed override Int32 Cd() { return 20; }
}
Classes declared sealed cannot be subclassed at all:
sealed class tqMgrSumm: tqMgr {
public override Int32 Cd() { return 30; }
}
Generics allow code to be reused without casting or unnecessary boxing. Classes, structures, interfaces, methods, and delegates can be defined as generics.
Several generics may share the same name so long as they accept differing numbers of generic arguments:
static void eExec<xz>(xzKey azKey) {
...
static void eExec<xzKey, xzData>(xzKey azKey, xzData azData) {
...
struct tPt<xz> {
public tPt(xz azX, xz azY) { zX = azX; zY = azY; }
public xz zX, zY;
}
static void Main() {
tPt<byte> oPt = new tPt<byte>(1, 2);
...
interface fSet<xz> {
void Add(xz az);
xz this[Int32 aIdx] { get; }
}
class tqSet: fSet<byte> {
public void Add(byte a) { ... }
public byte this[Int32 aIdx] {
get { ... }
}
}
public static void sSwap<xz>(ref xz arz0, ref xz arz1) {
xz oz0 = arz0;
arz0 = arz1;
arz1 = oz0;
}
When generic methods are called, the generic argument list may be omitted if the target is unambiguous:
sSwap(ref oPt1, ref oPt2);
otherwise, it must be specified:
sSwap<tPt<byte>>(ref oPt1, ref oPt2);
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));
...
Constraints limit the types allowed to be specified as arguments for a given generic parameter. In classes, structures, and interfaces, constraints are specified after the type parameter list:
struct tSet: fSet {
public void Add(byte a) {
...
class tqFiltAll: tqFilt {
public override bool vCk() {
...
class tHand<xqData, xqFilt>
where xqData: fSet, new()
where xqFilt: tqFilt {
public tHand(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;
No member of a parameter type can be accessed within the generic unless a constraint guarantees its presence.
Specifying class or struct mandates that the parameter type be a class or structure.
Specifying a class mandates that the parameter type be that class or a subclass thereof. The class may be specified with another type parameter; this is known as a naked type constraint:
interface fTbl<xqEl> {
List<xqElCd> ElsCd<xqElCd>(byte Cd)
where xqElCd: xqEl;
}
Specifying an interface mandates that the parameter type implement that interface.
Specifying new() mandates that the parameter type provide a parameterless constructor.
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:
static bool eCk(string aqName) {
...
static void Main() {
tdCk odCk = new tdCk(eCk);
...
Delegate instances are created implicitly by assigning a method directly to the varible:
tdCk odCk = eCk;
or by adding a method to a null variable:
tdCk odCk = null; odCk += eCk;
The referenced method can thereafter be invoked through the variable:
bool oCk = false;
if (odCk != null) oCk = odCk("Echo");
Invoking a null variable causes a NullReferenceException to be thrown.
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.
Instances can be copied from one variable to another, but the variables must have the exact same type:
tdExt odExt = eExtTop; tdExt odExtNext = odExt;
Different delegate types are never compatible, even when they share the same signature.
Delegate invocations pass from the delegate to the referenced method. For this reason, delegates can be assigned with methods having parameter types that are less specific than those of the delegate. Actual parameters meeting the delegate's requirements will necessarily meet those of the method.
Return values pass from the method back to the delegate. For this reason, delegates can be assigned with methods having return types that are more specific than those of the delegate. Return values meeting the method's requirement will necessarily meet that of the delegate:
class tqRsc {
...
class tqSumm {
...
static tqSumm eSumm(object aq) {
...
delegate object tdHand(tqRsc aqRsc);
static void Main() {
tdHand odHand = eSumm;
...
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;
Declaring a delegate variable as an event:
class tqCast {
public event tdNote dAdd_Note;
public void Add(Int32 aIdx) {
if (dAdd_Note != null) dAdd_Note(aIdx);
...
prevents methods outside the containing class from invoking the variable or modifying it, except with operator+= and operator-=:
static void eNote(Int32 aIdx) {
...
public static void Main() {
tqCast oqCast = new tqCast();
oqCast.dAdd_Note += eNote;
...
By default, events are implemented with a hidden backing variable and accessors, much as automatic properties are. These can also be explicitly defined:
tdNote edAdd_Note;
public event tdNote dAdd_Note {
add { edAdd_Note += value; }
remove { edAdd_Note -= value; }
}
When this has been done, even the containing class is unable to invoke the event or modify it, except with operator+= and operator-=; other operations must target the backing variable directly. When an interface-specified event is explicitly implemented, its accessors must be defined this way.
Events can be static, abstract, virtual, overridden, or sealed.
Lambda expressions are unnamed methods 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 aIdx) => (char)(aIdx + 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 parameter list parentheses can be omitted:
tdChar od = aIdx => (char)(aIdx + 32);
A set of generic delegates are defined within the System namespace to facilitate the creation of lambda expressions. Those named Func return a value; those named Action do not. Both sets are overloaded to accept up to four parameters with distinct types:
Func<byte, char> od = aIdx => (char)(aIdx + 32);
Variables in the scope defining some 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() {
Int32 oCt = 0;
dCd = (Int16 aX, Int16 aY) => {
++oCt;
return string.Format("{0}: {1}/{2}", oCt, aX, aY);
};
}
}
Anonymous methods are similar to lambda expressions. One is defined much like any other method, but its name and return type is replaced with the delegate keyword. The definition is then assigned to a delegate variable:
delegate byte tdEncr(byte a);
tdEncr od = delegate (byte a) {
return (byte)(a ^ 0xAA);
};
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.
Only System.Exception and its descendants can be thrown.
Exception implements a number of properties documenting the error condition. Message stores a description of the exception; it is set when the exception is created. Source stores the name of the application. StackTrace stores a string representation of the call stack at the time the exception was thrown. HelpLink stores the address of a resource providing help with the exception. InnerException stores the exception that caused the current exception, if any.
To catch all exceptions, specify Exception in the catch block:
catch (Exception aqExc) {
...
Only the first block matching a particular exception is executed. If multiple blocks are defined, blocks catching more specific exceptions should be listed first:
catch (DivideByZeroException aqExc) {
...
}
catch (Exception aqExc) {
...
The exception variable can be omitted if the instance is not needed:
catch (OutOfMemoryException) {
...
The variable and type can be omitted if the instance is not needed, and all exceptions are to be caught:
catch {
...
Exceptions can be rethrown from the catch block with throw:
catch (Exception aqExc) {
...
throw;
}
Exceptions cannot be rethrown from a finally block.
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.
The Language-Integrated Query system supports complex inlined queries with static syntax checking and static type checking. It is compatible with any collection implementing IEnumerable<> or IQueryable<>. 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<> instances with the Cast and OfType extension methods. Both iterate the sequence, converting all elements deemed compatible by is. Cast throws if an incompatible element is found:
IEnumerable oqObjs = new object[] { 5, 4, "X" };
IEnumerable<Int32> oqCts = oqObjs.Cast<Int32>();
OfType skips such elements:
oqCts = oqObjs.OfType<Int32>();
Cast and OfType can also be used to convert from one IEnumerable<> specialization to another.
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 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 called statically:
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());
Many queries can also be expressed as query expressions, with a syntax resembling SQL:
oqStarsSel =
from oq in oqStars
where oq.StartsWith("A")
select oq;
Query expressions are converted to a series of operator calls by the compiler. Some queries are more easily written as expressions, but not all operators can be represented this way.
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 sequence iteration.
Iteration variables can reference other iteration variables defined before them:
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;
Such expressions are implemented with SelectMany:
oqStarsSel = oqStars
.SelectMany(oqStar => oqTags, (oqStar, oqTag)
=> (oqStar + ' ' + oqTag));
let defines a new variable that is not iterated:
oqStarsSel = from oq in oqStars let oLen = oq.Length where oLen > 6 select oq + ": " + oLen;
let expressions are implemented by projecting the new variables, along with the original iteration variables, into anonymous types:
oqStarsSel = oqStars
.Select(oq => new { qOrig = oq, New = oq.Length })
.Where(oq => oq.New > 6)
.Select(oq => oq.qOrig + ": " + oq.New);
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;
These is implemented with OrderBy, OrderByDescending, ThenBy, and ThenByDescending:
oqPtsSel = oqPts .OrderBy(o => o.X) .ThenByDescending(o => o.Y);
Every expression ends with select or group.
When into follows select or group, a query continuation is defined. A continuation forwards the output of one query to another in the same expression, allowing additional operations to follow select or group, where expressions normally end:
oqStarsSel =
from oq in oqStars
select oq.ToUpper()
into oqUp
where oqUp.Contains("ALT")
select oqUp;
into serves as a from clause in the continuing query, introducing a new iteration variable to replace its predecessors, which are no longer in scope.
Continuations are implemented simply by chaining the continuing operators:
oqStarsSel = oqStars
.Select(oq => oq.ToUpper())
.Where(oq => oq.Contains("ALT"));
The join clause introduces a new iteration variable and a filter equating an expression derived from one or more outer variables with one derived from the new inner variable. The expressions must be specified in this order, for the outer variables are in scope only to the left of equals, and the inner variable only to the right:
oqStarsSel = from oqStar in oqStars join oqTag in oqTags on oqStar[0] equals oqTag[0] select oqStar + ' ' + oqTag;
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 containing multiple iteration variables:
oqStarsSel = from oqStar in oqStars from oqTag in oqTags where oqStar[0] == oqTag[0] select oqStar + ' ' + oqTag;
Joins explicitly defined with join or join can offer better performance, however. When applied to local collections, these operators represent inner sequences with hashtables. In other joins, sequences must be enumerated with nested loops.
Though joins and other multi-sequence expressions iterate multidimensional spaces, their output is usually 'flattened' into linear sequences.
When into follows a join clause, however, it defines a group join, which does not flatten its output:
Int32[] 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 corresponding to an outer iteration value. The inner sequences store inner iteration values grouped by the outer values to which they were joined:
foreach (IEnumerable<string> oqStarsLen in oqStarsByLen)
foreach (string oqStar in oqStarsLen)
Console.WriteLine(oqStar);
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 with the outer iteration variables that define them:
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 oqStarsLen in oqStarsByLen) {
Console.WriteLine(oqStarsLen.Len + ":");
foreach (string oqStar in oqStarsLen.qStars)
Console.WriteLine(oqStar);
}
Group joins return structures reflecting the multidimensionality inherent to all join operations. group, by contrast, converts linear sequences into two-dimensional structures:
IEnumerable<IGrouping<Int32, string>> oqStarsByLen = from oqStar in oqStars group oqStar by oqStar.Length;
Instead of returning an IEnumerable of IEnumerable, as group joins do, group returns an IEnumerable of IGrouping, which interface adds a Key property to IEnumerable:
public interface IGrouping<xzKey, xzEl>: IEnumerable<xzEl>,
IEnumerable {
xzKey Key { get; }
}
This addition eliminates the need to project into an anonymous type to store the outer iteration variable, as is often done with group joins:
foreach (IGrouping<Int32, string> oqStarsLen in oqStarsByLen) {
Console.WriteLine(oqStarsLen.Key + ":");
foreach (string oqStar in oqStarsLen)
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<Int32, string>> oqStarsByLen = from oqStar in oqStars group oqStar by oqStar.Length into oq where oq.Count() > 1 select oq;
Operators returning a single element or value are executed immediately, as are conversion operators like ToArray. Other operators are executed only when and as they 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()));
Empty returns an empty sequence.
Repeat returns a sequence containing the specified value repeated a certain number of times.
Range returns a sequence containing the specified integer and all those following it, up to the specified count.
Concat returns all elements in the sequence followed by all elements in the specified 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 sequence that do not appear in the specified sequence. Union, Intersect, and Except define overloads accepting an IEqualityComparer<>.
Projection operators transform and project the elements in 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 being transformed together into output elements.
Other SelectMany overloads flatten the sequences without transforming the pairs.
Where returns elements for which the specified predicate returns true:
oqStarsSel = oqStars.Where(oq => oq.Contains("us"));
Take returns the specified number of elements from the beginning of the sequence. Skip returns all elements except the specified number from the beginning.
TakeWhile returns all elements until the predicate returns false, at which point the iteration is stopped. SkipWhile ignores elements until the predicate returns true, then returns that element and all that follow it.
Distinct returns one instance of each element, with no duplicates. One overload accepts an IEqualityComparer<> to be when identifying duplicates.
Reverse returns the sequence in reverse order.
OrderBy and OrderByDescending return the sequence sorted. 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<> to be used when sorting.
Join transforms elements from the sequence and the specified inner sequence to create join key pairs. Matching pairs are then transformed into the output sequence:
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<> to be used when comparing keys.
GroupBy partitions the sequence into groups. Some overloads return an IEnumerable<> sequence of IGrouping<> sequences, each containing elements found to have the same key, as defined by the specified key function:
IEnumerable<IGrouping<Int32, string>> oqStarsByLen = oqStars.GroupBy(oq => oq.Length);
IGrouping<> derives from IEnumerable<>, to which it adds the Key property:
foreach (IGrouping<Int32, 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<> sequences. They transform each group and its key together into a single value, which is returned as part of an IEnumerable<> sequence:
IEnumerable<string> oqCtsByLen = oqStars
.GroupBy(oq => oq.Length,
(oLen, oqStarsLen) => (oLen.ToString() + ": "
+ oqStarsLen.Count()));
Others specify an IEqualityComparer<> to be used when comparing element keys. Still others implement several of these variations at once.
ToArray and ToList convert the sequence to an array or list.
ToDictionary and ToLookup convert the sequence to a Dictionary<> or Lookup<> instance. Both accept functions used to transform sequence elements into key values. Both define overloads that transform the new collection members, or specify an IEqualityComparer<> to be used when comparing element keys.
AsEnumerable downcasts the sequence to IEnumerable<>. AsQueryable converts the sequence to IQueryable<>.
Contains returns true if the sequence includes the specified element. SequenceEqual returns true if the sequence matches the specified sequence. Both define overloads specifying an IEqualityComparer<>.
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 specified predicate returns true for all elements.
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 with 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. If a predicate is specified, only elements for which the predicate returns true are considered.
DefaultIfEmpty returns all elements if the sequence is non-empty. If it is empty, it returns a sequence containing the default value, or the value passed to DefaultIfEmpty, if one is specified.
Count returns the element count, or the number of elements for which the predicate returns true, if one is specified. LongCount works as Count does, but it returns an Int64 instead of an Int32.
Min, Max, Sum, and Average return values describing the sequence:
Int32 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 with the value returned from the last call. Other overloads specify an initial value, or another function that transform the final value.
unsafe code can evade certain constraints imposed by the compiler or runtime, supporting increased performance, interoperability, or the management of memory outside the managed heap. To compile unsafe code, the /unsafe compiler flag must be set.
C# pointer syntax matches that of C++:
tPt oPt = new tPt(1, 2); tPt* opPt = &oPt; string oqText = opPt->ToString();
Pointers can reference value types and other pointers. They cannot reference classes or other managed types.
As in C++, void pointers are used to reference memory without declaring 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;
Pointers cannot be defined or used except within elements declared unsafe. When a class, structure, or interface is thus declared, its members gain the ability to define and use pointers. This extends to the bodies of methods defined in unsafe classes and structures:
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 be, but entire statement blocks can:
public void Exec() {
unsafe {
Int32* op;
...
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.
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 borne by 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.
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 assigned to reference stackalloc buffers and their elements just as they are assigned to fixed buffers.
Many generic collection interfaces and classes are also defined in non-generic forms.
Most generic collection interfaces and classes are defined within System.Collections.Generic, though ILookup<>, IGrouping<>, and Lookup<> are defined within System.Linq. Non-generic collections are defined within System.Collections.
IEnumerable<> specifies a collection that can be enumerated with foreach. It defines one member, GetEnumerator, that returns an IEnumerator<>.
IEnumerator<> specifies a forward-only cursor that enumerates a 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 this position.
No provision is made for the modification of a collection by either interface.
ICollection<> derives from IEnumerable<>. It adds a Count property, a Contains method that returns true if the specified value is part of the collection, and a CopyTo method that copies elements from the collection to an array.
More significantly, ICollection<> 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 to match the specified value, returning true if a match was found. Add, Clear, and Remove all throw if IsReadOnly returns false.
Types implementing ICollection<> can be initialized as arrays are:
List<char> oqChs = new List<char> { 'A', 'B', 'C' };
IList<> derives from ICollection<>. 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 matching the specified value, or negative one if no such element was found.
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<> implements IList<>. It provides the features and performance of ArrayList with type safety and little or no boxing. Methods like Contains and Remove box value types to gain access to object.Equals, however, if the element type does not implement IEquatable<>. Similarly, methods like Sort box value types if the type does not implement IComparable<>, and if no IComparer<> is passed to the method.
LinkedList<> implements ICollection<> with a double-linked list.
IDictionary<> derives from ICollection<>, as specialized with KeyValuePair<>. It specifies a collection that maps from 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.
ILookup<> derives from IEnumerable<>, as specialized with IGrouping<>. 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<> containing the corresponding values.
Dictionary<> implements IDictionary<> and ICollection<KeyValuePair<>> with a hash table. It maps unique keys to single values. If an IEqualityComparer<> is passed to the Dictionary<> constructor, that instance is used when comparing keys; if not, the key type's IEquatable<> implementation is used, or the default comparer, if IEquatable<> is not implemented.
Hashtable implements IDictionary and ICollection, also with a hash table. Key/value pairs are associated with DictionaryEntry instances, and this type must be used when enumerating the collection. Because it is not generic, the collection suffers from the same type-safety and boxing issues that afflict ArrayList.
Types serving as hash keys must override object.Equals and object.GetHashCode if they are not to rely on reference equality and the default object hash code. Hashtable allows hashes to be defined by an IHashCodeProvider, but this interface is considered obsolete.
Like Dictionary<>, SortedList<> and SortedDictionary<> implement IDictionary<> and ICollection<KeyValuePair<>>. SortedList<> does this with a sorted array, however, while SortedDictionary<> uses a binary search tree. If an IComparer<> is specified when constructing either type, that instance is used when comparing keys. If not, the key type's IComparable<> implementation is used, or the default comparer, if IComparable<> is not implemented. The classes are similar in most respects, but SortedList<> uses less memory, and supports fast random access to the key and value sets. SortedDictionary<> supports faster insertion and deletion of unsorted elements.
Lookup<> implements ILookup<> and IEnumerable<IGrouping<>>, serving to map unique keys to collections of values. Unlike those of other collections, Lookup<> instances are not created and populated on their own, for the class provides no public constructor, and its instances are immutable. Instead, instances are created and returned by IEnumeration.ToLookup.
System.Text.StringBuilder performs complex string operations without creating numerous temporary instances, as string instances 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 are read or written with the StringBuilder indexer. Various predefined types can be added with the Insert or Append methods. Strings composed with format strings are added with AppendFormat.
Character ranges are 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.
The static System.Convert class converts various predefined types to other predefined types.
All types derive ultimately from System.Object, aliased with object. All types can be upcast to object, but because it is a class, and therefore 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();
null does not represent an instance, so it cannot implement any member, even ToString.
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 the type itself. It cannot be used with an instance:
oq = typeof(tPt);
object.GetHashCode is meant to return hash codes suitable for use with collections like Hashtable and Dictionary. The base implementation is not considered reliable for user types, however, and such types should override GetHashCode if they are to serve as hash table keys. If two instances are considered equal by their Equals implementation, this override must return the same value for both. It is not necessary that two instances returning the same code be equal, but it is desirable. For performance reasons, it is also desirable that GetHashCode values be evenly distributed throughout their range.
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.
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.
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 such to ReferenceEquals causes them to be boxed, and the comparison therefore always fails, even when instances are compared with themselves.
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 applied to the first instance:
public static bool Equals(object, object);
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, however, 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, however, and for this reason, the ValueType implementation is somewhat slow. It is commonly overridden in value types to improve performance.
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, however, 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.
To avoid the costs incurred by reflection, the non-static Equals method is usually overloaded, and the equality operators redefined in value types:
struct tPt {
public Int32 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() {
return base.GetHashCode();
}
}
Replacing the non-static Equals method, the equality operator, or the inequality operator generally entails that all three be replaced, along with GetHashCode.
using statements ensure that classes or structures implementing IDisposable are disposed of when they go out of scope:
using (StreamWriter oqStream = File.CreateText(oqPath)) {
...
}
This is implemented by the compiler as:
StreamWriter oqStream = File.CreateText(oqPath);
try {
...
}
finally {
if (oqStream != null) ((IDisposable)oqStream).Dispose();
}
Given an instance of 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 both techniques are incompletely 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.
System.Nullable<> wraps value types in a structure that is able to represent null values. It cannot be used with classes, which are inherently nullable in any event.
Nullable types are created by specializing Nullable<>, or by marking value types with ?:
Int32? oLen = null;
Non-nullable types can be implicitly converted to nullable types:
oLen = 10;
When converting a nullable type to a non-nullable type, however, the Value property must be invoked:
Int32 oDist = oLen.Value;
or the instance must be explicitly cast:
Int32 oDist = (Int32)oLen;
Though casts are commonly used in such situations, the Value property offers greater type safety, and should be preferred.
If an attempt is made to extract a value from a null instance, InvalidOperationException will be thrown. The null-coalescing operator can be used to convert a nullable type without danger of an exception:
oDist = oLen ?? 0;
If the instance is non-null, the operator will return its value. Otherwise, it will return the specified default value. This operation is equivalent to Nullable<>.GetValueOrDefault when a default is specified.
Because they are referenced, boxed instances are inherently nullable. Therefore, when nullable types are boxed, only the underlying value type is copied to the heap.
Nullable<> does not implement the equality, comparison, or other operators that value types commonly do. After ensuring that both operands are non-null, however, the compiler forwards such calls to operators in the underlying type; this is called operator lifting. Null values, when encountered, are handled differently by different operators.
Nullable<> equality operators return true if both instances are null, or if neither is null and the lifted operator returns true. Nullable<> inequality operators return true if only one operand is null, or if neither is null and the lifted operator returns true.
Nullable<> 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<> operators, including the arithmetic and bitwise operators, return null if any operand is null. When Nullable<> 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<>.
Attributes attach metadata to types, members, and other code elements. They are created by subclassing System.Attribute:
class LinkAttribute: Attribute {
...
They are applied by specifying the attribute name in square brackets before the target element. If the name ends with 'Attribute', as is customary, 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() {
...
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 their signature must match one of the attribute class constructors:
[Part(0)] public Int32 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 Int32 Ct;
An attribute class can be made compatible with specific code elements by adding AttributeUsage to its definition. The AttributeTargets values passed to the AttributeUsage constructor can be combined arbitrarily:
[AttributeUsage(AttributeTargets.Field | AttributeTargets
.Property)]
class TagAttribute: Attribute {
...
AttributeUsage can also allow or disallow multiple applications of the same attribute to a given element, or enable or disable the inheritance of an attribute application from one class to another.
At run time, attributes can be read from Type or MemberInfo instances with GetCustomAttributes. They can be read from other code elements with Attribute.GetCustomAttribute and Attribute.GetCustomAttributes.
C# is not preprocessed as such, but these directives generally work as they do in C++.
Preprocessor directives do not end with semicolons, and cannot share lines with other instructions.
#define creates a symbol for use by other preprocessor directives. No value can be associated with the symbol, and it cannot be accessed except by other directives. #undef clears a defined symbol.
#if, #else, #elif, and #endif cause lines to be conditionally compiled. No '#ifndef' directive is provided, but symbols can be checked for non-existence with !:
#if !mSilent
Console.WriteLine("Done");
#endif
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 define outlining regions.
#line resets 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:
#line default
The hidden option causes all code up to the next #line directive to be skipped by the debugger. Such code is still executed, but the debugger does not stop within it:
#line hidden Exec(); #line default
#pragma directives forward other instructions to the compiler:
class tqMsg {
#pragma warning disable 0649
public string Text;
#pragma warning restore 0649
...
The entry point for any C# program is a static method named Main. The method can accept a string array or no parameters. It can return an Int32 or void:
public static Int32 Main() {
return 0;
}
Main can be defined within any class. It is commonly declared public, but it will function as an entry point no matter what its access level.
Ordinarily, the compiler will not allow more than one method to meet these criteria. If more than one must be defined, the 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. It does not store the executable name:
public static void Main(string[] aqArgs) {
foreach (string oq in aqArgs)
Console.WriteLine(oq);
}
An assembly can contain an application and/or one or more libraries.
C# programs can be built from the command line by invoking the compiler directly.
Selecting 'Tools / Visual Studio Command Prompt' within Visual Studio opens a command line with the relevant environment variables already set. Programs are built by passing one or more C# files to the compiler, 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
C# 3.0 Pocket Reference
Joseph Albahari, Ben Albahari
2008, O'Reilly Media
MSDN Library
Retrieved October 2009
Professional C# 2005
Christian Nagel, Bill Evjen, Jay Glynn, Morgan Skinner, Karli Watson, Allen Jones
2006, Wiley Publishing