Chapter 2 - C# Language Basics
A First C# Program
A First C# Program
class Test // Class declaration { static void Main() // Method declaration { int x = 12 * 30; // Statement 1 Console.WriteLine (x); // Statement 2 } // End of method }
First Program Refactored
// Here, we've refactored the logic in our original main method into a method called FeetToInches. static void Main() { Console.WriteLine (FeetToInches (30)); // 360 Console.WriteLine (FeetToInches (100)); // 1200 } static int FeetToInches (int feet) { int inches = feet * 12; return inches; }
Syntax Basics
The @ prefix
// If you really want to use a keyword as an identifier, you can do so with the @ prefix. // This can be useful for language interoperability. int @class = 123; string @namespace = "foo";
Contextual Keywords
// The identifiers below are examples of *contextual* keywords, so we can use them without conflict: int add = 3; bool ascending = true; int yield = 45;
Semicolons and Comments
// Statements can span multiple lines, thanks to the semicolon terminator: Console.WriteLine (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10); int x = 3; // Single-line comment int y = 3; /* This is a comment that spans two lines */
Type Basics
Predefined Type Examples
// string, int and bool types are examples of predefined types: string message = "Hello world"; string upperMessage = message.ToUpper(); Console.WriteLine (upperMessage); // HELLO WORLD int x = 2015; message = message + x.ToString(); Console.WriteLine (message); // Hello world2015 bool simpleVar = false; if (simpleVar) Console.WriteLine ("This will not print"); int y = 5000; bool lessThanAMile = y < 5280; if (lessThanAMile) Console.WriteLine ("This will print");
Custom Type Examples
// Just as we can build complex functions from simple functions, we can build complex types // from primitive types. UnitConverter serves a a blueprint for unit conversions: public class UnitConverter { int ratio; // Field public UnitConverter (int unitRatio) { ratio = unitRatio; } // Constructor public int Convert (int unit) { return unit * ratio; } // Method } static void Main() { UnitConverter feetToInchesConverter = new UnitConverter (12); UnitConverter milesToFeetConverter = new UnitConverter (5280); Console.WriteLine (feetToInchesConverter.Convert (30)); // 360 Console.WriteLine (feetToInchesConverter.Convert (100)); // 1200 Console.WriteLine (feetToInchesConverter.Convert (milesToFeetConverter.Convert(1))); // 63360 }
Instance vs Static Members
// The instance field Name pertains to an instance of a particular Panda, // whereas Population pertains to the set of all Pandas: public class Panda { public string Name; // Instance field public static int Population; // Static field public Panda (string n) // Constructor { Name = n; // Assign the instance field Population = Population + 1; // Increment the static Population field } } static void Main() { Panda p1 = new Panda ("Pan Dee"); Panda p2 = new Panda ("Pan Dah"); Console.WriteLine (p1.Name); // Pan Dee Console.WriteLine (p2.Name); // Pan Dah Console.WriteLine (Panda.Population); // 2 }
Conversions
// Implicit conversions are allowed when the compiler can guarantee they will // always succeed and no information is lost in conversion: int x = 12345; // int is a 32-bit integer long y = x; // Implicit conversion to 64-bit integer // In other cases, you need explicit conversions: short z = (short)x; // Explicit conversion to 16-bit integer x.Dump ("x"); y.Dump ("y"); z.Dump ("z");
Value Types
// The content of a value type variable or constant is simply a value. // You can define a custom value type with the struct keyword: public struct Point { public int X, Y; } static void Main() { Point p1 = new Point(); p1.X = 7; Point p2 = p1; // Assignment causes copy Console.WriteLine (p1.X); // 7 Console.WriteLine (p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine (p1.X); // 9 Console.WriteLine (p2.X); // 7 }
Reference Types
// A reference type has two parts: an object and the reference to that object. public class Point { public int X, Y; } static void Main() { Point p1 = new Point(); p1.X = 7; Point p2 = p1; // Copies p1 *reference* Console.WriteLine (p1.X); // 7 Console.WriteLine (p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine (p1.X); // 9 Console.WriteLine (p2.X); // 9 }
Null
// A reference can be assigned the literal null, indicating that the reference points to nothing: public class Point { public int X, Y; } static void Main() { Point p = null; Console.WriteLine (p == null); // True // The following line generates a runtime error (a NullReferenceException is thrown): Console.WriteLine (p.X); }
Nulls with structs
// A value type cannot ordinarily have a null value: public struct Point { public int X, Y; } static void Main() { Point p = null; // This line will not compile. int x = null; // Illegal, too. } // See "Nullable Types" in Chapter 4 for a workaround.
Storage Overhead
// Structs take up as much room as their fields: struct Point { int x; // 4 bytes int y; // 4 bytes } // However, the CLR requires that fields are offset within the type at an address // that’s a multiple of their size: struct A { byte b; // 1 byte long l; // 8 bytes } unsafe static void Main() { sizeof (Point).Dump(); // 8 bytes sizeof (A).Dump(); // 16 bytes }
Numeric Types
Numeric Types
// The signed integral types are sbyte, short, int, long: int i = -1; i.Dump(); // The unsigned integral types are byte, ushort, uint and ulong: byte b = 255; b.Dump(); // The real types are float, double and decimal: double d = 1.23; d.Dump(); // (See book for a table comparing each of the numeric types)
Numeric Literals
// Integral literals can use decimal or hexadecimal notation; hexadecimal is denoted with the 0x prefix: int x = 127; long y = 0x7F; //From C# 7, you can insert an underscore anywhere inside a numeric literal to make it more readable: int million = 1_000_000; //C# 7 also lets you specify numbers in binary with the 0b prefix: var b = 0b1010_1011_1100_1101_1110_1111; //Real literals can use decimal and/or exponential notation. For example: double d = 1.5; double doubleMillion = 1E06; // Numeric literal type inference: Console.WriteLine ( 1.0.GetType()); // Double (double) Console.WriteLine ( 1E06.GetType()); // Double (double) Console.WriteLine ( 1.GetType()); // Int32 (int) Console.WriteLine (0xF0000000.GetType()); // UInt32 (uint) Console.WriteLine (0x100000000.GetType()); // Int64 (long)
Numeric Suffixes
// Numeric literals can be suffixed with a character to indicate their type: // F = float // D = double // M = decimal // U = uint // L = long // UL = ulong long i = 5; // No suffix needed: Implicit lossless conversion from int literal to long // The D suffix is redundant in that all literals with a decimal point are inferred to be double: double x = 4.0; // The F and M suffixes are the most useful: float f = 4.5F; // Will not compile without the F suffix decimal d = -1.23M; // Will not compile without the M suffix.
Numeric Conversions
// Integral conversions are implicit when the destination type can represent every possible value // of the source type. Otherwise, an explicit conversion is required: int x = 12345; // int is a 32-bit integral long y = x; // Implicit conversion to 64-bit integral short z = (short)x; // Explicit conversion to 16-bit integral // All integral types may be implicitly converted to all floating-point numbers: int i = 1; float f = i; // The reverse conversion must be explicit: int iExplicit = (int)f; // Implicitly converting a large integral type to a floating-point type preserves magnitude but may // occasionally lose precision: int i1 = 100000001; float f1 = i1; // Magnitude preserved, precision lost int i2 = (int)f1; // 100000000
Increment and Decrement Operators
// The increment and decrement operators (++, --) increment and decrement numeric types by 1. // The operator can either precede or follow the variable, depending on whether you want the // value before or after the increment/decrement: int x = 0, y = 0; Console.WriteLine (x++); // Outputs 0; x is now 1 Console.WriteLine (++y); // Outputs 1; x is now 1
Integral Division
// Integral division truncates remainders: int a = 2 / 3; // 0 // Division by zero is an error: int b = 0; int c = 5 / b; // throws DivisionByZeroException
Integral Overflow
// By default, integral arithmetic operations overflow silently: int a = int.MinValue; a--; Console.WriteLine (a == int.MaxValue); // True
Overflow Checking
// You can add the checked keyword to force overflow checking: int a = 1000000; int b = 1000000; // The following code throws OverflowExceptions: int c = checked (a * b); // Checks just the expression. // Checks all expressions in statement block: checked { int c2 = a * b; c2.Dump(); }
Overflow Checking with Constant Expressions
// Compile-time overflows are special in that they're checked by default: int x = int.MaxValue + 1; // Compile-time error // You have to use unchecked to disable this: int y = unchecked (int.MaxValue + 1); // No errors
8- and 16-bit literals
// The 8- and 16-bit integral types are byte, sbyte, short, and ushort. These types lack their // own arithmetic operators, so C# implicitly converts them to larger types as required. // This can cause a compile-time error when trying to assign the result back to a small integral type: short x = 1, y = 1; short z = x + y; // Compile-time error // In this case, x and y are implicitly converted to int so that the addition can be performed. // To make this compile, we must add an explicit cast: short z = (short) (x + y); // OK
Special float and double Values
// Reminder when using LINQPad: You can highlight any section of code and // hit F5 to execute just that selection! // Unlike integral types, floating-point types have values that certain operations treat specially, // namely NaN (Not a Number), +∞, ?∞, and ?0: Console.WriteLine (double.NegativeInfinity); // -Infinity // Dividing a nonzero number by zero results in an infinite value: Console.WriteLine ( 1.0 / 0.0); // Infinity Console.WriteLine (-1.0 / 0.0); // -Infinity Console.WriteLine ( 1.0 / -0.0); // -Infinity Console.WriteLine (-1.0 / -0.0); // Infinity // Dividing zero by zero, or subtracting infinity from infinity, results in a NaN: Console.WriteLine ( 0.0 / 0.0); // NaN Console.WriteLine ((1.0 / 0.0) - (1.0 / 0.0)); // NaN // When using ==, a NaN value is never equal to another value, even another NaN value: Console.WriteLine (0.0 / 0.0 == double.NaN); // False // To test whether a value is NaN, you must use the float.IsNaN or double.IsNaN method: Console.WriteLine (double.IsNaN (0.0 / 0.0)); // True // When using object.Equals, however, two NaN values are equal: Console.WriteLine (object.Equals (0.0 / 0.0, double.NaN)); // True
Real Number Rounding Errors
// Unlike decimal, float and double can cannot precisely represent numbers with a base-10 // fractional component: { float tenth = 0.1f; // Not quite 0.1 float one = 1f; Console.WriteLine (one - tenth * 10f); // -1.490116E-08 } { decimal tenth = 0.1m; // Exactly 0.1 decimal one = 1m; Console.WriteLine (one - tenth * 10m); // 0.0 } // Neither double nor decimal can precisely represent a fractional number whose base 10 // representation is recurring: decimal m = 1M / 6M; // 0.1666666666666666666666666667M double d = 1.0 / 6.0; // 0.16666666666666666 m.Dump ("m"); d.Dump ("d"); // This leads to accumulated rounding errors: decimal notQuiteWholeM = m+m+m+m+m+m; // 1.0000000000000000000000000002M double notQuiteWholeD = d+d+d+d+d+d; // 0.99999999999999989 // which breaks equality and comparison operations: Console.WriteLine (notQuiteWholeM == 1M); // False Console.WriteLine (notQuiteWholeD < 1.0); // True
Boolean Type and Operators
Equality and Comparison Operators
// == and != test for equality and inequality of any type, but always return a bool value // (unless overloaded otherwise). Value types typically have a very simple notion of equality: int x = 1; int y = 2; int z = 1; Console.WriteLine (x == y); // False Console.WriteLine (x != y); // True Console.WriteLine (x == z); // True Console.WriteLine (x < y); // True Console.WriteLine (x >= z); // True
Equality with Reference Types
// For reference types, equality, by default, is based on reference, as opposed to the // actual value of the underlying object (more on this in Chapter 6). static void Main() { Dude d1 = new Dude ("John"); Dude d2 = new Dude ("John"); Console.WriteLine (d1 == d2); // False Dude d3 = d1; Console.WriteLine (d1 == d3); // True } public class Dude { public string Name; public Dude (string n) { Name = n; } }
And & Or Operators
// The && and || operators test for and and or conditions. They are frequently used in // conjunction with the ! operator, which expresses not: static bool UseUmbrella (bool rainy, bool sunny, bool windy) { return !windy && (rainy || sunny); } static void Main() { UseUmbrella (true, false, false).Dump(); // True UseUmbrella (true, true, true).Dump(); // False }
Shortcircuiting
// The && and || operators short-circuit. This is essential in allowing expressions such as // the following to run without throwing a NullReferenceException: StringBuilder sb = null; if (sb != null && sb.Length > 0) Console.WriteLine ("sb has data"); else Console.WriteLine ("sb is null or empty");
And & Or Operators - non-shortcircuiting
// Same examples as before, but with & and | instead of && and ||. // The results are identical, but without short-circuiting: static bool UseUmbrella (bool rainy, bool sunny, bool windy) { return !windy & (rainy | sunny); } static void Main() { UseUmbrella (true, false, false).Dump(); // True UseUmbrella (true, true, true).Dump(); // False StringBuilder sb = null; if (sb != null & sb.Length > 0) // Exception is thrown! Console.WriteLine ("sb has data"); else Console.WriteLine ("sb is null or empty"); }
Conditional operator (ternary)
// The conditional operator (also called the ternary operator) has the form // q ? a : b // where if condition q is true, a is evaluated, else b is evaluated. static int Max (int a, int b) { return (a > b) ? a : b; } static void Main() { Max (2, 3).Dump(); Max (3, 2).Dump(); }
Strings and Characters
Character literals
// C#’s char type represents a Unicode character and occupies two bytes. char c = 'A'; // Simple character // Escape sequences express characters that cannot be expressed or interpreted literally. // An escape sequence is a backslash followed by a character with a special meaning: char newLine = '\n'; char backSlash = '\\'; c.Dump(); (backSlash.ToString() + newLine.ToString() + backSlash.ToString()).Dump();
Character conversions
// An implicit conversion from a char to a numeric type works for the numeric types that can // accommodate an unsigned short: ushort us = 'a'; int i = 'z'; us.Dump(); i.Dump(); // For other numeric types, an explicit conversion is required short s = (short) 'a'; s.Dump();
String literals
// A string literal is specified inside double quotes: string h = "Heat"; // string is a reference type, rather than a value type. Its equality operators, however, // follow value-type semantics: string a = "test"; string b = "test"; Console.WriteLine (a == b); // True // The escape sequences that are valid for char literals also work inside strings: string t = "Here's a tab:\t"; // The cost of this is that whenever you need a literal backslash, you must write it twice: string a1 = "\\\\server\\fileshare\\helloworld.cs"; a1.Dump ("a1"); // To avoid this problem, C# allows "verbatim string literals" - prefixed with @ symbols: string a2 = @"\\server\fileshare\helloworld.cs"; a2.Dump ("a2"); // A verbatim string literal can also span multiple lines: string escaped = "First Line\r\nSecond Line"; string verbatim = @"First Line Second Line"; // Assuming your IDE uses CR-LF line separators: Console.WriteLine (escaped == verbatim); // True // You can include the double-quote character in a verbatim literal by writing it twice: string xml = @"<customer id=""123""></customer>"; xml.Dump ("xml");
String concatenation
// The + operator concatenates two strings: string s1 = "a" + "b"; s1.Dump(); // The righthand operand may be a nonstring value, in which case ToString is called on that value: string s2 = "a" + 5; // a5 s2.Dump();
String interpolation
// A string preceded with the $ character is an interpolated string: int x = 4; Console.WriteLine ($"A square has {x} sides"); // Prints: A square has 4 sides string s = $"255 in hex is {byte.MaxValue:X2}"; // X2 = 2-digit Hexadecimal s.Dump ("With a format string"); x = 2; s = $@"this spans { x} lines"; s.Dump ("Verbatim multi-line interpolated string");
Arrays
Arrays
// An array represents a fixed number of elements of a particular type. char[] vowels = new char[5]; // Declare an array of 5 characters // Square brackets also index the array, accessing a particular element by position: vowels [0] = 'a'; vowels [1] = 'e'; vowels [2] = 'i'; vowels [3] = 'o'; vowels [4] = 'u'; Console.WriteLine (vowels [1]); // e // Array indexes start at 0. We can use a for loop statement to iterate through each element in the array. // The for loop in this example cycles the integer i from 0 to 4: for (int i = 0; i < vowels.Length; i++) Console.Write (vowels [i]); // aeiou // An array initialization expression: char[] easy = {'a','e','i','o','u'}; easy.Dump();
Default Element Initialization
// Creating an array always preinitializes the elements with default values. // For int, this is 0: int[] a = new int[1000]; Console.Write (a [123]); // 0
Default Element Initialization - Reference Types
// In contrast, creating an array of reference types allocates null references: public class Point { public int X, Y; } static void Main() { Point[] a = new Point [1000]; for (int i = 0; i < a.Length; i++) // Iterate i from 0 to 999 a [i] = new Point(); // Set array element i with new point Point[] nulls = new Point [1000]; Console.WriteLine (nulls [0] == null); // True Console.WriteLine (nulls [0].X); // Error: NullReferenceException thrown }
Default Element Initialization - Value Types
// For arrays, when the element type is a value type, each element value is allocated // as part of the array: public struct Point { public int X, Y; } static void Main() { Point[] a = new Point[1000]; int x = a[500].X; // 0 x.Dump(); }
Indices
char[] vowels = new char[] {'a','e','i','o','u'}; char lastElement = vowels [^1].Dump(); // 'u' char secondToLast = vowels [^2].Dump(); // 'o' Index first = 0; Index last = ^1; char firstElement = vowels [first].Dump(); // 'a' char lastElement2 = vowels [last].Dump(); // 'u'
Ranges
char[] vowels = new char[] { 'a', 'e', 'i', 'o', 'u' }; char[] firstTwo = vowels [..2].Dump(); // 'a', 'e' char[] lastThree = vowels [2..].Dump(); // 'i', 'o', 'u' char[] middleOne = vowels [2..3].Dump(); // 'i' char[] lastTwo = vowels [^2..].Dump(); // 'o', 'u' Range firstTwoRange = 0..2; char[] firstTwo2 = vowels [firstTwoRange].Dump(); // 'a', 'e'
Multidimensional Arrays - Rectangular
// Rectangular arrays represent an n-dimensional block of memory; jagged arrays are arrays of arrays. int [,] matrix = new int [3, 3]; // 2-dimensional rectangular array // The GetLength method of an array returns the length for a given dimension (starting at 0): for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) matrix [i, j] = i * 3 + j; matrix.Dump(); // A rectangular array can be initialized as follows: int[,] matrix2 = new int[,] { {0,1,2}, {3,4,5}, {6,7,8} }; matrix2.Dump();
Multidimensional Arrays - Jagged
// Here's how to declare a jagged array (an array of arrays): int [][] matrix = new int [3][]; // The inner dimensions aren’t specified in the declaration. Unlike a rectangular array, // each inner array can be an arbitrary length. Each inner array is implicitly initialized // to null rather than an empty array. Each inner array must be created manually: for (int i = 0; i < matrix.Length; i++) { matrix[i] = new int [3]; // Create inner array for (int j = 0; j < matrix[i].Length; j++) matrix[i][j] = i * 3 + j; } matrix.Dump ("Populated manually"); // A jagged array can be initialized as follows: int[][] matrix2 = new int[][] { new int[] {0,1,2}, new int[] {3,4,5}, new int[] {6,7,8,9} }; matrix2.Dump ("Populated via array initialization expression");
Simplified Array Initialization Expressions
char[] vowels = {'a','e','i','o','u'}; // We can omit the "new" expression after the assignment operator: int[,] rectangularMatrix = { {0,1,2}, {3,4,5}, {6,7,8} }; int[][] jaggedMatrix = { new int[] {0,1,2}, new int[] {3,4,5}, new int[] {6,7,8} }; rectangularMatrix.Dump(); jaggedMatrix.Dump();
Simplified Array Initialization with Implicit Typing
// The var keyword tells the compiler to implicitly type a local variable: var i = 3; // i is implicitly of type int var s = "sausage"; // s is implicitly of type string // Therefore: var rectMatrix = new int[,] // rectMatrix is implicitly of type int[,] { {0,1,2}, {3,4,5}, {6,7,8} }; var jaggedMat = new int[][] // jaggedMat is implicitly of type int[][] { new int[] {0,1,2}, new int[] {3,4,5}, new int[] {6,7,8} }; // Implicit typing can be taken one stage further with single-dimensional arrays. You can omit // the type qualifier after the new keyword and have the compiler infer the array type: var vowels = new[] {'a','e','i','o','u'}; // Compiler infers char[] var x = new[] { 1, 10000000000 }; // Legal - all elements are convertible to long vowels.Dump(); x.Dump();
Bounds Checking
// All array indexing is bounds-checked by the runtime: int[] arr = new int[3]; arr[3] = 1; // IndexOutOfRangeException thrown
Variables and Parameters
Stack
// The stack is a block of memory for storing local variables and parameters. static void Main() { Factorial(5).Dump(); } // For each call to Factorial, x gets pushed onto the stack: static int Factorial (int x) { if (x == 0) return 1; return x * Factorial (x-1); }
Heap
// The heap is a block of memory in which objects (i.e., reference-type instances) reside. // The runtime has a garbage collector that periodically deallocates objects from the heap. StringBuilder ref1 = new StringBuilder ("object1"); Console.WriteLine (ref1); // The StringBuilder referenced by ref1 is now eligible for GC. StringBuilder ref2 = new StringBuilder ("object2"); StringBuilder ref3 = ref2; // The StringBuilder referenced by ref2 is NOT yet eligible for GC. Console.WriteLine (ref3); // object2
Definite Assignment - Local Variables
// C#'s Definite Assignment policy means that local variables must be initialized before use. int x; Console.WriteLine (x); // Compile-time error
Definite Assignment - Array Elements
// Array elements are automatically initialized: int[] ints = new int[2]; Console.WriteLine (ints[0]); // 0
Definite Assignment - Fields
// Fields are automatically initialized: static int x; static void Main() { Console.WriteLine (x); // 0 }
Parameters - Passing by Value
// By default, arguments in C# are passed by value. // This means a copy of the value is created when passed to the method: static void Foo (int p) { p = p + 1; // Increment p by 1 Console.WriteLine ("p is " + p); // Write p to screen } static void Main() { int x = 8; Foo (x); // Make a copy of x Console.WriteLine ("x is " + x); // x will still be 8 }
Parameters - Passing by Value (reference types)
// Passing a reference-type argument by value copies the reference, not the object: static void Foo (StringBuilder fooSB) { fooSB.Append ("test"); fooSB = null; } static void Main() { StringBuilder sb = new StringBuilder(); Foo (sb); Console.WriteLine (sb.ToString()); // test }
Parameters - The ref Modifier
// To pass by reference, C# provides the ref parameter modifier. // In the following example, p and x refer to the same memory locations: static void Foo (ref int p) { p = p + 1; // Increment p by 1 Console.WriteLine (p); // Write p to screen } static void Main() { int x = 8; Foo (ref x); // Ask Foo to deal directly with x Console.WriteLine (x); // x is now 9 }
Parameters - The ref Modifier - Swap Method
// The ref modifier is essential in implementing a swap method: static void Swap (ref string a, ref string b) { string temp = a; a = b; b = temp; } static void Main() { string x = "Penn"; string y = "Teller"; Swap (ref x, ref y); Console.WriteLine (x); // Teller Console.WriteLine (y); // Penn }
Parameters - The out Modifier
// The out modifier is most commonly used to get multiple return values back from a method: static void Split (string name, out string firstNames, out string lastName) { int i = name.LastIndexOf (' '); firstNames = name.Substring (0, i); lastName = name.Substring (i + 1); } static void Main() { string a, b; Split ("Stevie Ray Vaughn", out a, out b); Console.WriteLine (a); // Stevie Ray Console.WriteLine (b); // Vaughn }
Parameters - out variables and discards
// From C# 7, you can declare variables on the fly when calling methods with out parameters. static void Main() { Split ("Stevie Ray Vaughan", out string a, out string b); Console.WriteLine (a); // Stevie Ray Console.WriteLine (b); // Vaughan Split ("Stevie Ray Vaughan", out string x, out _); // Discard the 2nd param Console.WriteLine (x); } static void Split (string name, out string firstNames, out string lastName) { int i = name.LastIndexOf (' '); firstNames = name.Substring (0, i); lastName = name.Substring (i + 1); }
Parameters - The in Modifier
void Main() { SomeBigStruct x = default; Foo (x); // Calls the first overload Foo (in x); // Calls the second overload Bar (x); // OK (calls the 'in' overload) Bar (in x); // OK (calls the 'in' overload) } void Foo (SomeBigStruct a) => "Foo".Dump(); void Foo (in SomeBigStruct a) => "in Foo".Dump(); void Bar (in SomeBigStruct a) => "in Bar".Dump(); struct SomeBigStruct { public decimal A, B, C, D, E, F, G; }
Parameters - Implications of Passing By Reference
// In the following example, the variables x and y represent the same instance: static int x; static void Main() { Foo (out x); } static void Foo (out int y) { Console.WriteLine (x); // x is 0 y = 1; // Mutate y Console.WriteLine (x); // x is 1 }
Parameters - The params modifier
// The params parameter modifier on the last parameter of a method accepts any number of parameters // of a specified type: static int Sum (params int[] ints) { int sum = 0; for (int i = 0; i < ints.Length; i++) sum += ints[i]; // Increase sum by ints[i] return sum; } static void Main() { int total = Sum (1, 2, 3, 4); Console.WriteLine (total); // 10 // The call to Sum above is equivalent to: int total2 = Sum (new int[] { 1, 2, 3, 4 } ); }
Parameters - Optional Parameters
// From C# 4.0, methods, constructors and indexers can declare optional parameters. // A parameter is optional if it specifies a default value in its declaration: static void Foo (int x = 23) { Console.WriteLine (x); } static void Main() { Foo(); // 23 Foo (23); // 23 (equivalent to above call) }
Parameters - Named Arguments
// Rather than identifying an argument by position, you can identify it by name: static void Foo (int x, int y) { Console.WriteLine (x + ", " + y); } static void Main() { Foo (x:1, y:2); // 1, 2 Foo (y:2, x:1); // 1, 2 (semantically same as above) // You can mix named and positional arguments: Foo (1, y:2); }
Parameters - Optional Parameters with Named Arguments
// Named arguments are particularly useful in conjunction with optional parameters: static void Bar (int a = 0, int b = 0, int c = 0, int d = 0) { Console.WriteLine (a + " " + b + " " + c + " " + d); } static void Main() { Bar (d:3); }
ref locals
// C# 7 added an esoteric feature, whereby you can define a local variable that references // an element in an array or field in an object. int[] numbers = { 0, 1, 2, 3, 4 }; ref int numRef = ref numbers [2]; // In this example, numRef is a reference to the numbers [2].When we modify numRef, // we modify the array element: numRef *= 10; Console.WriteLine (numRef); // 20 Console.WriteLine (numbers [2]); // 20
ref returns
// You can return a ref local from a method. This is called a ref return: static string X = "Old Value"; static ref string GetX() => ref X; // This method returns a ref static void Main() { ref string xRef = ref GetX(); // Assign result to a ref local xRef = "New Value"; Console.WriteLine (X); // New Value }
var - Implicitly Typed Variables
// The contextual keyword var implicitly types local variables: { var x = "hello"; var y = new System.Text.StringBuilder(); var z = (float)Math.PI; } // This is precisely equivalent to: { string x = "hello"; System.Text.StringBuilder y = new System.Text.StringBuilder(); float z = (float)Math.PI; }
Implicitly Typed Variables are Statically Typed
// Implicitly typed variables are statically typed! var x = 5; x = "hello"; // Compile-time error; x is of type int
Implicitly Typed Variables and Readability
var sb = new System.Text.StringBuilder(); // Type of sb is obvious var z = (float)Math.PI; // Type of z is obvious Random r = new Random(); var x = r.Next(); // What type is x?
Expressions and Operators
Primary Expressions
// This is a primary expression. Notice the "Language" dropdown above is set to "Expression" - this // allows a pure expression to execute in LINQPad without extra baggage: Math.Log(1)
Assignment Expressions
// An assignment expression is not a void expression. It actually carries the assignment // value, and so can be incorporated into another expression: int x, y; y = 5 * (x = 2); x.Dump(); y.Dump(); x *= 2; // equivalent to x = x * 2 x <<= 1; // equivalent to x = x << 1 x.Dump();
Precedence
// The * operator has higher precedence than + so this expression evaluates to 7: 1 + 2 * 3 // (See book for operator precedence table)
Left Associativity
// For operators of the same precedence, associativity determines order of evaluation. // The binary operators (except for assignment, lambda and null coalescing operators) are // left-associative; in other words, they are evaluated from left to right: 8 / 4 / 2
Right Associativity
// The assignment operators, lambda, null coalescing and conditional operator are right-associative: int x, y; x = y = 3; x.Dump(); y.Dump();
Null Operators
Null Coalescing Operator
string s1 = null; string s2 = s1 ?? "nothing"; // s2 evaluates to "nothing" s2.Dump();
Null Coalescing Assignment Operator
string s1 = null; s1 ??= "something"; Console.WriteLine (s1); // something s1 ??= "everything"; Console.WriteLine (s1); // something
Null-Conditional Operator
System.Text.StringBuilder sb = null; string s = sb?.ToString(); // No error; s instead evaluates to null s.Dump(); string s2 = sb?.ToString().ToUpper(); // s evaluates to null without error s2.Dump();
Null-Conditional Operator - with nullable types
System.Text.StringBuilder sb = null; int? length = sb?.ToString().Length; // OK : int? can be null length.Dump(); string s = sb?.ToString() ?? "nothing"; // s evaluates to "nothing" s.Dump();
Statements
Declaration Statements
// You may declare multiple variables of the same type in a comma-separated list: string someWord = "rosebud"; int someNumber = 42; bool rich = true, famous = false;
Declaration Statements - Constants
const double c = 2.99792458E08; c += 10; // Compile-time Error
Declaration Statements - Local Variables
// The scope of a local or constant variable extends throughout the current block: int x; { int y; int x; // Error - x already defined } { int y; // OK - y not in scope } Console.Write (y); // Error - y is out of scope
Expression Statements
// Expression statements are expressions that are also valid statements. // Declare variables with declaration statements: string s; int x, y; System.Text.StringBuilder sb; // Expression statements x = 1 + 2; // Assignment expression x++; // Increment expression y = Math.Max (x, 5); // Assignment expression Console.WriteLine (y); // Method call expression sb = new StringBuilder(); // Assignment expression new StringBuilder(); // Object instantiation expression
if statement
if (5 < 2 * 3) Console.WriteLine ("true"); // True
else clause
if (2 + 2 == 5) Console.WriteLine ("Does not compute"); else Console.WriteLine ("false"); // False // If/else statements can be nested: if (2 + 2 == 5) Console.WriteLine ("Does not compute"); else if (2 + 2 == 4) Console.WriteLine ("Computes"); // Computes // The above is commonly formatted as follows: if (2 + 2 == 5) Console.WriteLine ("Does not compute"); else if (2 + 2 == 4) Console.WriteLine ("Computes"); // Computes
Changing Execution Flow with Braces
// An else clause always applies to the immediately preceding if statement in the statement block: if (true) if (false) Console.WriteLine(); else Console.WriteLine ("executes"); // This is semantically identical to: if (true) { if (false) Console.WriteLine(); else Console.WriteLine ("executes"); } // We can change the execution flow by moving the braces: if (true) { if (false) Console.WriteLine(); } else Console.WriteLine ("does not execute");
Omitting Braces
// Braces don't necessarily help readability. The following is clear without braces: static void TellMeWhatICanDo (int age) { if (age >= 35) Console.WriteLine ("You can be president!"); else if (age >= 21) Console.WriteLine ("You can drink!"); else if (age >= 18) Console.WriteLine ("You can vote!"); else Console.WriteLine ("You can wait!"); } static void Main() { TellMeWhatICanDo (55); TellMeWhatICanDo (30); TellMeWhatICanDo (20); TellMeWhatICanDo (8); }
switch Statement
// switch statements may result in cleaner code than multiple if statements: static void ShowCard (int cardNumber) { switch (cardNumber) { case 13: Console.WriteLine ("King"); break; case 12: Console.WriteLine ("Queen"); break; case 11: Console.WriteLine ("Jack"); break; case -1: // Joker is -1. goto case 12; // In this game joker counts as queen. default: // Executes for any other cardNumber. Console.WriteLine (cardNumber); break; } } static void Main() { ShowCard (5); ShowCard (11); ShowCard (13); }
switch Statement - Stacking Cases
// When more than one value should execute the same code, you can list the common cases sequentially: int cardNumber = 12; switch (cardNumber) { case 13: case 12: case 11: Console.WriteLine ("Face card"); break; default: Console.WriteLine ("Plain card"); break; }
switch Statement - patterns
// From C# 7, you can switch on multiple types. static void Main() { TellMeTheType (12); TellMeTheType ("hello"); TellMeTheType (true); } static void TellMeTheType (object x) // object allows any type. { switch (x) { case int i: Console.WriteLine ("It's an int!"); Console.WriteLine ($"The square of {i} is {i * i}"); break; case string s: Console.WriteLine ("It's a string"); Console.WriteLine ($"The length of {s} is {s.Length}"); break; default: Console.WriteLine ("I don't know what x is"); break; } }
switch Statement - patterns - predicated
object x = true; switch (x) { case bool b when b == true: // Fires only when b is true Console.WriteLine ("True!"); break; case bool b: Console.WriteLine ("False!"); break; }
switch Statement - patterns - stacked
object x = 3000m; switch (x) { case float f when f > 1000: case double d when d > 1000: case decimal m when m > 1000: Console.WriteLine ("We can refer to x here but not f or d or m"); break; }
switch expressions
int cardNumber = 13; string cardName = cardNumber switch { 13 => "King", 12 => "Queen", 11 => "Jack", _ => "Pip card" // equivalent to 'default' }; cardName.Dump(); string suite = "spades"; string cardName2 = (cardNumber, suite) switch // tuple pattern { (13, "spades") => "King of spades", (13, "clubs") => "King of clubs", _ => "Other" }; cardName2.Dump();
while loop
// With while loops, the expression is tested before the body of the loop is executed: int i = 0; while (i < 3) { Console.WriteLine (i); i++; }
do-while loop
// With a do-while loop, the check is performed at the end, so the body always executes at least once: int i = 0; do { Console.WriteLine (i); i++; } while (i < 3);
for loop
// Simple for-loop: for (int i = 0; i < 3; i++) Console.WriteLine (i); Console.WriteLine(); // You can have more than one variable in the initialization clause: for (int i = 0, prevFib = 1, curFib = 1; i < 10; i++) { Console.WriteLine (prevFib); int newFib = prevFib + curFib; prevFib = curFib; curFib = newFib; }
foreach loop
// The foreach statement iterates over each element in an enumerable object. // The following works because System.String implements IEnumerable<char>: foreach (char c in "beer") // c is the iteration variable Console.WriteLine (c);
break statement
// The break statement ends the execution of the body of an iteration or switch statement: int x = 0; while (true) { if (x++ > 5) break ; // break from the loop } x.Dump();
continue statement
// The continue statement forgoes the remaining statements in a loop and makes an // early start on the next iteration: for (int i = 0; i < 10; i++) { if ((i % 2) == 0) // If i is even, continue; // continue with next iteration Console.Write (i + " "); }
goto statement
// C# supports goto - in case you really want it! int i = 1; startLoop: if (i <= 5) { Console.Write (i + " "); i++; goto startLoop; }
return statement
// A return statement can appear anywhere in a method. static void Main() { AsPercentage (0.345m).Dump(); } static decimal AsPercentage (decimal d) { decimal p = d * 100m; return p; // Return to the calling method with value }
Namespaces
Nesting namespaces
void Main() { typeof (Outer.Middle.Inner.Class1).FullName.Dump(); } namespace Outer { namespace Middle { namespace Inner { class Class1 {} class Class2 {} } } }
Using directive
// Note: to add a 'using' directive in LINQPad, go to query properties or press Ctrl+Shift+M void Main() { Class1 c; // Don’t need fully qualified name } namespace Outer { namespace Middle { namespace Inner { class Class1 {} class Class2 {} } } }
Using static
// Press Ctrl+Shift+M to see the using static import. void Main() { WriteLine ("Hello"); }
Rules - Name scoping
namespace Outer { class Class1 { } namespace Inner { class Class2 : Class1 { } } } namespace MyTradingCompany { namespace Common { class ReportBase { } } namespace ManagementReporting { class SalesReport : Common.ReportBase { } } }
Rules - Name hiding
namespace Outer { class Foo { } namespace Inner { class Foo { } class Test { Foo f1; // = Outer.Inner.Foo Outer.Foo f2; // = Outer.Foo } } }
Rules - Repeated namespaces
namespace Outer.Middle.Inner { class Class1 {} } namespace Outer.Middle.Inner { class Class2 {} }
Rules - Nested using directive
namespace N1 { class Class1 {} } namespace N2 { using N1; class Class2 : Class1 {} } namespace N2 { class Class3 : Class1 { } // Compile-time error }
Aliasing types and namespaces
// Press Ctrl+Shift+M to see the using static import. void Main() { PropertyInfo2 p; R.PropertyInfo p2; }
Namespace alias qualifier
namespace N { class A { static void Main() { new A.B().Dump(); // Instantiate nested class B new global::A.B().Dump(); // Instantiate class B in namespace A } public class B { } // Nested type } } namespace A { class B { } }