Chapter 2 - C# Language Basics
A First C# Program
A First C# Program
int x = 12 * 30; // Statement 1
System.Console.WriteLine (x); // Statement 2
A First C# Program with using directive
using System;
int x = 12 * 30; // Statement 1
Console.WriteLine (x); // Statement 2
First Program Refactored
// Here, we've refactored the logic in our original main method into a method called FeetToInches.
Console.WriteLine (FeetToInches (30)); // 360
Console.WriteLine (FeetToInches (100)); // 1200
int FeetToInches (int feet)
{
int inches = feet * 12;
return inches;
}
Method with no input or output
SayHello();
void SayHello()
{
Console.WriteLine ("Hello, world");
}
A First C# Program - without top-level statements
using System;
class Program
{
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:
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
public class UnitConverter
{
int ratio; // Field
public UnitConverter (int unitRatio) { ratio = unitRatio; } // Constructor
public int Convert (int unit) { return unit * ratio; } // Method
}
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:
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
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
}
}
Defining a namespace
// The same code, but with Panda defined inside a namespace.
using Animals;
Panda p = new Panda ("Pan Dee");
Console.WriteLine (p.Name);
namespace Animals
{
public class Panda
{
public string Name;
public Panda (string n) // Constructor
{
Name = n; // Assign the instance field
}
}
}
Defining a Main method
// Here's our original program, without using top-level statements.
// (In LINQPad, we set the language in the toolbar above to 'C# Program'.)
using System;
class Program
{
static void Main() // Program entry point
{
int x = 12 * 30;
Console.WriteLine (x);
}
}
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:
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
public struct Point { public int X, Y; }
Reference Types
// A reference type has two parts: an object and the reference to that object.
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
public class Point { public int X, Y; }
Null
// A reference can be assigned the literal null, indicating that the reference points to nothing:
Point p = null;
Console.WriteLine (p == null); // True
// The following line generates a runtime error (a NullReferenceException is thrown):
Console.WriteLine (p.X);
public class Point { public int X, Y; }
Nulls with structs
// A value type cannot ordinarily have a null value:
Point p = null; // This line will not compile.
int x = null; // Illegal, too.
public struct Point { public int X, Y; }
// See "Nullable Types" in Chapter 4 for a workaround.
Storage Overhead
// Structs take up as much room as their fields:
unsafe static void Main()
{
sizeof (Point).Dump(); // 8 bytes
sizeof (A).Dump(); // 16 bytes
}
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
}
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 x = 0.1f; // Not quite 0.1
Console.WriteLine (x + x + x + x + x + x + x + x + x + x); // 1.0000001
}
{
decimal y = 0.1m; // Exactly 0.1
Console.WriteLine (y + y + y + y + y + y + y + y + y + y); // 1.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).
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:
UseUmbrella (true, false, false).Dump(); // True
UseUmbrella (true, true, true).Dump(); // False
bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
return !windy && (rainy || sunny);
}
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:
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");
bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
return !windy & (rainy | sunny);
}
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.
Max (2, 3).Dump();
Max (3, 2).Dump();
int Max (int a, int b)
{
return (a > b) ? a : b;
}
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");
Raw string literals
// Wrapping a string in three or more quote characters (""") creates a raw string literal.
// Raw string literals can contain almost any character sequence, without escaping or doubling up:
string raw = """<file path="c:\temp\test.txt"></file>""";
raw.Dump ("Raw string literal");
// Should you need to include three (or more) quote characters in the string itself, you can do so
// by wrapping the string in four (or more) quote characters:
raw = """"The """ sequence denotes raw string literals."""";
raw.Dump ("This string includes three quotes");
// Multiline raw string literals are subject to special rules.
// The string, "Line 1\r\nLine 2", we can represent as follows:
string multiLineRaw = """
Line 1
Line 2
""";
multiLineRaw.Dump ("multiLineRaw");
// Notice that the opening and closing quotes must be on separate lines to the string content. Additionally:
//
// • Whitespace following the opening """ (on the same line) is ignored.
//
// • Whitespace preceding the closing """ (on the same line) is treated as common indentation and is removed
// from every line in the string. This lets you include indentation for source-code readability without
// that indentation becoming part of the string.
if (true)
if (true)
if (true)
if (true)
{
string json = """
{
"Name" : "Joe"
}
""";
json.Dump ("JSON");
}
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");
String interpolation and constants
// Legal from C# 10:
const string greeting = "Hello";
const string message = $"{greeting}, world";
message.Dump();
String interpolation with raw string literals
// To include a brace literal in an interpolated string:
// • With standard and verbatim string literals, repeat the desired brace character;
// • With raw string literals, change the interpolation sequence by repeating the $ prefix.
// Using two (or more) $ characters in a raw string literal prefix changes the interpolation
// sequence from one brace to two (or more) braces:
$$"""{ "TimeStamp": "{{DateTime.Now}}" }""".Dump();
UTF-8 strings
// From C# 11, you can use the u8 suffix to create string literals encoded in UTF-8 rather than UTF-16.
// This feature is intended for advanced scenarios such as the low-level handling of JSON text in
// performance hotspots:
ReadOnlySpan<byte> utf8 = "ab→cd"u8; // Arrow symbol consumes 3 bytes
Console.WriteLine (utf8.Length); // 7
// The underlying type is ReadOnlySpan<byte>, which we cover in Chapter 23.
// You can convert this to an array by calling the ToArray() method.
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:
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
public class Point { public int X, Y; }
Default Element Initialization - Value Types
Point[] a = new Point[1000];
int x = a[500].X; // 0
x.Dump();
public struct Point { public int X, Y; }
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
// For each call to Factorial, x gets pushed onto the stack:
Factorial(5).Dump();
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:
Console.WriteLine (Test.X); // 0
class Test { public static int X; } // field
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:
int x = 8;
Foo (x); // Make a copy of x
Console.WriteLine ("x is " + x); // x will still be 8
void Foo (int p)
{
p = p + 1; // Increment p by 1
Console.WriteLine ("p is " + p); // Write p to screen
}
Parameters - Passing by Value (reference types)
// Passing a reference-type argument by value copies the reference, not the object:
StringBuilder sb = new StringBuilder();
Foo (sb);
Console.WriteLine (sb.ToString()); // test
static void Foo (StringBuilder fooSB)
{
fooSB.Append ("test");
fooSB = null;
}
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:
int x = 8;
Foo (ref x); // Ask Foo to deal directly with x
Console.WriteLine (x); // x is now 9
static void Foo (ref int p)
{
p = p + 1; // Increment p by 1
Console.WriteLine (p); // Write p to screen
}
Parameters - The ref Modifier - Swap Method
// The ref modifier is essential in implementing a swap method:
string x = "Penn";
string y = "Teller";
Swap (ref x, ref y);
Console.WriteLine (x); // Teller
Console.WriteLine (y); // Penn
static void Swap (ref string a, ref string b)
{
string temp = a;
a = b;
b = temp;
}
Parameters - The out Modifier
// The out modifier is most commonly used to get multiple return values back from a method:
string a, b;
Split ("Stevie Ray Vaughn", out a, out b);
Console.WriteLine (a); // Stevie Ray
Console.WriteLine (b); // Vaughn
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 - out variables and discards
// From C# 7, you can declare variables on the fly when calling methods with out parameters.
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);
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 - 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 in Modifier
// An in parameter is similar to a ref parameter except that the argument’s value cannot modified by the method.
// This is useful when passing a large value type to the method because it allows the compiler to avoid the overhead
// of copying the argument prior to passing it in while still protecting the original value from modification.
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;
}
Extra - Parameters - The ref readonly Modifier
// C# 12 also supports 'ref readonly' as an alternative to 'in'.
//
// 'ref readonly' behaves like 'in', but generates warnings when the compiler is
// unable to pass by reference, and is useful in advanced optimization scenarios.
//
// For a full discussion, see query://../../../What's_New_in_C#/What's_New_in_C#_12/Ref_readonly_parameters
Parameters - The params modifier
// The params parameter modifier on the last parameter of a method accepts any number of parameters
// of a specified type:
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 });
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;
}
Parameters - Optional Parameters
// Methods, constructors and indexers can declare optional parameters.
// A parameter is optional if it specifies a default value in its declaration:
Foo(); // 23
Foo (23); // 23 (equivalent to above call)
void Foo (int x = 23) { Console.WriteLine (x); }
Parameters - Named Arguments
// Rather than identifying an argument by position, you can identify it by name:
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);
void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }
Parameters - Optional Parameters with Named Arguments
// Named arguments are particularly useful in conjunction with optional parameters:
Bar (d:3);
void Bar (int a = 0, int b = 0, int c = 0, int d = 0)
{
Console.WriteLine (a + " " + b + " " + c + " " + d);
}
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;
}
Target-typed new expressions
// The contextual keyword var implicitly types local variables:
{
System.Text.StringBuilder sb1 = new();
System.Text.StringBuilder sb2 = new ("Test");
}
// This is precisely equivalent to:
{
System.Text.StringBuilder sb1 = new System.Text.StringBuilder();
System.Text.StringBuilder sb2 = new System.Text.StringBuilder ("Test");
}
// Target-typed new expressions are helpful when th emethod argument is a constructor call:
MyMethod (new ("test"));
void MyMethod (StringBuilder sb) { }
// They're also useful when the variable declaration and initialization are in different
// parts of your code:
class Foo
{
System.Text.StringBuilder sb;
public Foo (string initialValue)
{
sb = new (initialValue);
}
}
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();
string[] words = null;
string word = words? [1]; // word is null
word.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
TellMeWhatICanDo (55);
TellMeWhatICanDo (30);
TellMeWhatICanDo (20);
TellMeWhatICanDo (8);
static void TellMeWhatICanDo (int age)
{
// Braces don't necessarily help readability. The following is clear without braces:
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!");
}
switch Statement
// switch statements may result in cleaner code than multiple if statements:
ShowCard (5); ShowCard (11); ShowCard (13);
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;
}
}
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.
TellMeTheType (12);
TellMeTheType ("hello");
TellMeTheType (DateTime.Now);
TellMeTheType (true);
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;
case DateTime:
Console.WriteLine ("It's a DateTime");
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.
AsPercentage (0.345m).Dump();
decimal AsPercentage (decimal d)
{
decimal p = d * 100m;
return p; // Return to the calling method with value
}
Namespaces
Nesting namespaces
typeof (Outer.Middle.Inner.Class1).FullName.Dump();
namespace Outer
{
namespace Middle
{
namespace Inner
{
class Class1 {}
class Class2 {}
}
}
}
Using directive
// In LINQPad, you can also add a 'using' directive via Query Properties (press Ctrl+Shift+M)
using Outer.Middle.Inner;
Class1 c; // Don’t need fully qualified name
namespace Outer
{
namespace Middle
{
namespace Inner
{
class Class1 {}
class Class2 {}
}
}
}
Global using directive
// 'global using' directives apply to files in the project.
// (In LINQPad, global usings have the same effect as normal usings.)
global using Outer.Middle.Inner;
Class1 c; // Don’t need fully qualified name
namespace Outer
{
namespace Middle
{
namespace Inner
{
class Class1 {}
class Class2 {}
}
}
}
Using static
using static System.Console;
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
using PropertyInfo2 = System.Reflection.PropertyInfo;
using R = System.Reflection;
PropertyInfo2 p;
R.PropertyInfo p2;
Aliasing any type
// From C# 12, the using directive can alias any kind of type, including, for instance, arrays:
using NumberList = double[];
NumberList numbers = { 2.5, 3.5 };
numbers.Dump();
// You can also alias tuples — we cover this in “Aliasing Tuples” in Chapter 4.
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 { }
}