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

Native-sized integers

// The nint and unint native-sized integer types are sized to match the address space
// of the processor and operating system at runtime (in practice, 32 or 64 bits).

// Native-sized integers behave much like standard integers, with support
// for arithmetic operations, overflow check operators, and so on:

nint x = 123;
nint y = 234;
nint product = x * y;

product.Dump();

// You can implicitly convert between nint/unint and IntPtr/UIntPtr:

IntPtr p = x;
y = p;

// Arithmetic is supported *only* with nint/unint, so the following does not compile:
// (p * p).Dump();

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");

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:

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

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 - 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();

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 (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;
    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 {}
    }
  }
}

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;

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 { }
}
C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription