Chapter 3 - Creating Types in C#
Classes
Fields
// A field is a variable that is a member of a class or struct.
var o = new Octopus();
o.Age.Dump();
class Octopus
{
string name;
public int Age = 10;
static readonly int legs = 8, eyes = 1;
}
Fields - readonly
// Readonly fields let you create *immutable* classes.
var o = new Octopus ("Jack");
o.Name.Dump();
o.Legs = 20; // Compile-time error
class Octopus
{
public readonly string Name;
public readonly int Legs = 8;
public Octopus (string name)
{
Name = name;
}
}
Constants - scoped to class
// Constants are factored out at compile-time and baked into the calling site.
Test.Message.Dump();
public class Test
{
public const string Message = "Hello World";
}
Constants - scoped to method
// Here, the calculation is performed at compile-time:
const double twoPI = 2 * System.Math.PI;
twoPI.Dump();
Methods - Expression-bodied
// Foo1 and Foo2 are equivalent:
Foo1 (10).Dump();
Foo2 (10).Dump();
int Foo1 (int x) { return x * 2; }
int Foo2 (int x) => x * 2;
Local methods
void Main()
{
WriteCubes();
}
void WriteCubes()
{
Console.WriteLine (Cube (3));
Console.WriteLine (Cube (4));
Console.WriteLine (Cube (5));
int Cube (int value) => value * value * value;
}
Local methods - with top-level statements
// When you use top-level statements, your methods are implicitly local.
// We can verify this by trying to access a variable outside the method:
int x = 3;
Foo();
// Foo is a local method
void Foo() => Console.WriteLine (x); // We can access x
Methods - Overloading
// We can overload Foo as follows:
void Foo (int x) { "int".Dump(); }
void Foo (double x) { "double".Dump(); }
void Foo (int x, float y) { "int, float".Dump(); }
void Foo (float x, int y) { "float, int".Dump(); }
// We have to use a Main() method here to test this,
// because methods in top-level statements are *local methods*
// which cannot be overloaded.
void Main()
{
Foo (123); // int
Foo (123.0); // double
Foo (123, 123F); // int, float
Foo (123F, 123); // float, int
}
Methods - Illegal Overloading
// The following overloads are prohibited:
void Foo (int x);
float Foo (int x); // Compile-time error
void Goo (int[] x);
void Goo (params int[] x); // Compile-time error
void Hoo (int x);
void Hoo (ref int x); // OK so far
void Hoo (out int x); // Compile-time error
void Main() {}
Constructors
Panda p = new Panda ("Petey"); // Call constructor
public class Panda
{
string name; // Define field
public Panda (string n) // Define constructor
{
name = n; // Initialization code (set up field)
}
}
Constructors - Overloading
// You can also overload constructors.
// Note the use of the "this" keyword to call another constructor:
new Wine (78).Dump();
new Wine (78, 2001).Dump();
public class Wine
{
public decimal Price;
public int Year;
public Wine (decimal price) => Price = price;
public Wine (decimal price, int year) : this (price) => Year = year;
}
Constructors - Nonpublic
// A common reason to have a nonpublic constructor is to control instance creation via a
// static method call:
Class1 c1 = Class1.Create(); // OK
Class1 c2 = new Class1(); // Error: Will not compile
public class Class1
{
Class1() { } // Private constructor
public static Class1 Create()
{
// Perform custom logic here to create & configure an instance of Class1
/* ... */
return new Class1();
}
}
Deconstructors
// To call the deconstructor, we use the following special syntax:
var rect = new Rectangle (3, 4);
(float width, float height) = rect; // Deconstruction
Console.WriteLine (width + " " + height); // 3 4
// We can also use implicit typing:
var (x, y) = rect; // Deconstruction
Console.WriteLine (x + " " + y);
// If the variables already exist, we can do a *deconstructing assignment*:
(x, y) = rect;
Console.WriteLine (x + " " + y);
// The following mix-and-match is permitted, from C# 10:
double x1 = 0;
(x1, double y2) = rect;
class Rectangle
{
public readonly float Width, Height;
public Rectangle (float width, float height)
{
Width = width;
Height = height;
}
public void Deconstruct (out float width, out float height)
{
width = Width;
height = Height;
}
}
Object Initializers
// Fields or properties can be initialized in a single statement directly after construction:
// Object initialization syntax. Note that we can still specify constructor arguments:
Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };
b1.Dump(); b2.Dump();
public class Bunny
{
public string Name;
public bool LikesCarrots;
public bool LikesHumans;
public Bunny () {}
public Bunny (string n) { Name = n; }
}
Object Initializer Alternative - Optional Parameters
// Instead of using object initializers, we could make Bunny’s constructor accept optional parameters.
// This has both pros and cons (see book):
Bunny b = new Bunny (
name: "Bo",
likesCarrots: true);
b.Dump();
public class Bunny
{
public string Name;
public bool LikesCarrots;
public bool LikesHumans;
public Bunny (
string name,
bool likesCarrots = false,
bool likesHumans = false)
{
Name = name;
LikesCarrots = likesCarrots;
LikesHumans = likesHumans;
}
}
The this Reference
// The this reference refers to the instance itself:
new Panda().Marry (new Panda());
public class Panda
{
public Panda Mate;
public void Marry (Panda partner)
{
Mate = partner;
partner.Mate = this;
}
}
Properties
// Properties look like fields from the outside but internally, they contain logic, like methods:
var stock = new Stock();
stock.CurrentPrice = 123.45M;
stock.CurrentPrice.Dump();
var stock2 = new Stock { CurrentPrice = 83.12M };
stock2.CurrentPrice.Dump();
public class Stock
{
decimal currentPrice; // The private "backing" field
public decimal CurrentPrice // The public property
{
get { return currentPrice; } set { currentPrice = value; }
}
}
Properties - calculated & read-only
// The Worth Property is a read-only calculated property.
var stock = new Stock { CurrentPrice = 50, SharesOwned = 100 };
stock.Worth.Dump();
public class Stock
{
decimal currentPrice; // The private "backing" field
public decimal CurrentPrice // The public property
{
get { return currentPrice; } set { currentPrice = value; }
}
decimal sharesOwned; // The private "backing" field
public decimal SharesOwned // The public property
{
get { return sharesOwned; } set { sharesOwned = value; }
}
public decimal Worth
{
get { return currentPrice * sharesOwned; }
}
}
Properties - expression-bodied
// The Worth Property is now an expression-bodied property.
var stock = new Stock { CurrentPrice = 50, SharesOwned = 100 };
stock.Worth.Dump();
public class Stock
{
decimal currentPrice; // The private "backing" field
public decimal CurrentPrice // The public property
{
get { return currentPrice; } set { currentPrice = value; }
}
decimal sharesOwned; // The private "backing" field
public decimal SharesOwned // The public property
{
get { return sharesOwned; } set { sharesOwned = value; }
}
public decimal Worth => currentPrice * sharesOwned; // Expression-bodied property
// From C# 7, we can take this further, and write both the get and set accessors in
// expression-bodied syntax:
public decimal Worth2
{
get => currentPrice * sharesOwned;
set => sharesOwned = value / currentPrice;
}
}
Automatic Properties
// Here's the preceding example rewritten with two automatic properties:
var stock = new Stock { CurrentPrice = 50, SharesOwned = 100 };
stock.Worth.Dump();
public class Stock
{
public decimal CurrentPrice { get; set; } // Automatic property
public decimal SharesOwned { get; set; } // Automatic property
public decimal Worth
{
get { return CurrentPrice * SharesOwned; }
}
}
Property Initializers
var stock = new Stock();
stock.CurrentPrice.Dump();
stock.Maximum.Dump();
public class Stock
{
public decimal CurrentPrice { get; set; } = 123;
public int Maximum { get; } = 999;
}
Properties - get & set accessibility
// In this example, the set accessors are private while the get accessors are public:
new Foo { X = 5 }; // Will not compile - X has a private set accessor.
public class Foo
{
private decimal x;
public decimal X
{
get { return x; }
private set { x = Math.Round (value, 2); }
}
public int Auto { get; private set; } // Automatic property
}
Indexers
// You can implement custom indexers with the this keyword:
Sentence s = new Sentence();
Console.WriteLine (s[3]); // fox
s[3] = "kangaroo";
Console.WriteLine (s[3]); // kangaroo
// Test the indexers that use C#'s Indices and Ranges:
Console.WriteLine (s [^1]); // fox
string[] firstTwoWords = s [..2].Dump(); // (The, quick)
class Sentence
{
string[] words = "The quick brown fox".Split();
public string this [int wordNum] // indexer
{
get { return words [wordNum]; }
set { words [wordNum] = value; }
}
// In C# 8, we can also define indexers that use the Index & Range types:
public string this [Index index] => words [index];
public string[] this [Range range] => words [range];
}
Primary Constructors
// From C# 12, you can include a parameter list directly after a class (or struct) declaration.
// This instructs the compiler to automatically build a primary constructor using the primary
// constructor parameters (firstName and lastName).
Person p = new Person ("Alice", "Jones");
p.Print(); // Alice Jones
class Person (string firstName, string lastName)
{
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
// See also query://../Inheritance/Inheritance_with_primary_constructors
Primary Constructors - additional constructors
Person p = new Person ("Alice", "Jones", 33);
p.Print(); // Alice Jones
class Person (string firstName, string lastName)
{
int _age;
// The constructor that C# builds is called primary because any additional constructors
// that you choose to (explicitly) write must invoke it:
public Person (string firstName, string lastName, int age)
: this (firstName, lastName) // Must call the primary constructor
{
_age = age;
}
public void Print() => Console.WriteLine (firstName + " " + lastName + " " + _age);
}
// See also query://../Inheritance/Inheritance_with_primary_constructors
Primary Constructors and initializers
// The accessibility of primary constructor parameters extends to field and property initializers.
// In the following example, we use field and property initializers to assign firstName to a public
// field, and lastName to a public property:
Person p = new Person ("Alice", "Jones");
p.Dump();
class Person (string firstName, string lastName)
{
public readonly string FirstName = firstName; // Field
public string LastName { get; } = lastName; // Property
}
Primary Constructors - masking
// Fields (or properties) can reuse primary constructor parameter names:
Person p = new Person ("Alice", "Jones");
p.Print();
class Person (string firstName, string lastName)
{
// In this scenario, the field or property takes precedence, thereby masking
// the primary constructor parameter, except on the righthand side of field and
// property initializers:
readonly string firstName = firstName;
readonly string lastName = lastName;
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
Primary Constructors - validating
new Person1 ("Alice", "Jones").Print(); // Alice Jones
new Person2 ("Alice", "Jones").Print(); // Alice JONES
var p3 = new Person3 ("Alice", null); // throws ArgumentNullException
class Person1 (string firstName, string lastName)
{
// Sometimes it’s useful to perform computation in field initializers:
public readonly string FullName = firstName + " " + lastName;
public void Print() => Console.WriteLine (FullName);
}
class Person2 (string firstName, string lastName)
{
// Save an uppercase version of lastName to a field of the same name
// (masking the original value):
readonly string lastName = lastName.ToUpper();
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
class Person3 (string firstName, string lastName)
{
// Validate lastName upon construction, ensuring that it cannot be null:
readonly string lastName = (lastName == null)
? throw new ArgumentNullException ("lastName")
: lastName;
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
Static Constructors
// A static constructor executes once per type, rather than once per instance:
// Type is initialized only once
new Test();
new Test();
new Test();
class Test
{
static Test()
{
Console.WriteLine ("Type Initialized");
}
}
Static Constructors & Field Initialization Order
// Static field initializers run just before the static constructor is called:
Foo.X.Dump ("X"); // 0
Foo.Y.Dump ("Y"); // 3
class Foo
{
public static int X = Y; // 0
public static int Y = 3; // 3
}
Static Constructors & Field Initialization Order (Constructor Call)
// Another way to go awry:
Console.WriteLine (Foo.X); // 3
Util.NewProcess = true; // Force LINQPad to create new process on each run.
class Foo
{
public static Foo Instance = new Foo();
public static int X = 3;
Foo() => Console.WriteLine (X); // 0
}
Partial Types
// Partial types allow a type definition to be split—typically across multiple files:
new PaymentForm { X = 3, Y = 4 }.Dump();
partial class PaymentForm { public int X; }
partial class PaymentForm { public int Y; }
Partial Methods
// A partial type may contain partial methods. These let an auto-generated partial type
// provide customizable hooks for manual authoring.
var paymentForm = new PaymentForm (50);
partial class PaymentForm // In auto-generated file
{
public PaymentForm (decimal amount)
{
ValidatePayment (amount);
// ...
}
partial void ValidatePayment (decimal amount);
}
partial class PaymentForm // In hand-authored file
{
partial void ValidatePayment (decimal amount)
{
if (amount < 100)
throw new ArgumentOutOfRangeException ("amount", "Amount too low!");
}
}
Extended Partial Methods
// The presence of an accessiblity modifier on a partial method denotes an 'extended partial method'.
// Extended partial methods *must* have implementations:
public partial class Test
{
public partial void M1(); // Extended partial method
private partial void M2(); // Extended partial method
public partial bool IsValid (string identifier);
internal partial bool TryParse (string number, out int result);
}
public partial class Test
{
public partial void M1() { }
private partial void M2() {}
public partial bool IsValid (string identifier) => !string.IsNullOrEmpty (identifier);
internal partial bool TryParse (string number, out int result) => int.TryParse (number, out result);
}
The nameof operator
int count = 123;
nameof (count).Dump ("count");
nameof (StringBuilder.Length).Dump ("Length property on StringBuilder");
(nameof (StringBuilder) + "." + nameof (StringBuilder.Length)).Dump ("StringBuilder.Length");
Inheritance
Inheritance
// A class can inherit from another class to extend or customize the original class.
Stock msft = new Stock { Name = "MSFT", SharesOwned = 1000 };
Console.WriteLine (msft.Name); // MSFT
Console.WriteLine (msft.SharesOwned); // 1000
House mansion = new House { Name = "Mansion", Mortgage = 250000 };
Console.WriteLine (mansion.Name); // Mansion
Console.WriteLine (mansion.Mortgage); // 250000
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
Polymorphism
// A variable of type x can refer to an object that subclasses x.
// The Display method below accepts an Asset. This means means we can pass it any subtype:
Display (new Stock { Name="MSFT", SharesOwned=1000 });
Display (new House { Name="Mansion", Mortgage=100000 });
void Display (Asset asset)
{
Console.WriteLine (asset.Name);
}
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
Reference Conversions - Upcasting
// An upcast creates a base class reference from a subclass reference:
Stock msft = new Stock();
Asset a = msft; // Upcast
// After the upcast, the two variables still references the same Stock object:
Console.WriteLine (a == msft); // True
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
Reference Conversions - Downcasting
// A downcast operation creates a subclass reference from a base class reference.
Stock msft = new Stock();
Asset a = msft; // Upcast
Stock s = (Stock)a; // Downcast
Console.WriteLine (s.SharesOwned); // <No error>
Console.WriteLine (s == a); // True
Console.WriteLine (s == msft); // True
// A downcast requires an explicit cast because it can potentially fail at runtime:
House h = new House();
Asset a2 = h; // Upcast always succeeds
Stock s2 = (Stock)a2; // ERROR: Downcast fails: a is not a Stock
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
The is operator
// The is operator tests whether a reference conversion (or unboxing conversion) would succeed:
Asset a = new Asset();
if (a is Stock)
Console.WriteLine (((Stock)a).SharesOwned);
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
The is operator and pattern variables
// The is operator tests whether a reference conversion (or unboxing conversion) would succeed:
Asset a = new Stock { SharesOwned = 3 };
if (a is Stock s)
Console.WriteLine (s.SharesOwned);
// We can take this further:
if (a is Stock s2 && s2.SharesOwned > 100000)
Console.WriteLine ("Wealthy");
else
s2 = new Stock(); // s is in scope
Console.WriteLine (s2.SharesOwned); // Still in scope
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
The as operator
// The as operator performs a downcast that evaluates to null (rather than throwing an exception)
// if the downcast fails.
Asset a = new Asset();
Stock s = a as Stock; // s is null; no exception thrown
if (s != null) Console.WriteLine (s.SharesOwned); // Nothing written
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
Virtual Function Members
// A function marked as virtual can be overridden by subclasses wanting to provide a
// specialized implementation:
House mansion = new House { Name="McMansion", Mortgage=250000 };
Console.WriteLine (mansion.Liability); // 250000
public class Asset
{
public string Name;
public virtual decimal Liability => 0; // Virtual
}
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage; // Overridden
}
public class Stock : Asset
{
public long SharesOwned;
// We won't override Liability here, because the default implementation will do.
}
Covariant returns
// From C# 9, we can override a method such that it returns a more derived (subclased) type:
House mansion1 = new House { Name = "McMansion", Mortgage = 250000 };
House mansion2 = mansion1.Clone();
public class Asset
{
public string Name;
public virtual Asset Clone() => new Asset { Name = Name };
}
public class House : Asset
{
public decimal Mortgage;
// We can return House when overriding:
public override House Clone() => new House { Name = Name, Mortgage = Mortgage };
}
Abstract Classes & Members
// A class declared as abstract can never be instantiated. Instead, only its concrete subclasses
// can be instantiated. Abstract classes are able to define abstract members.
new Stock { SharesOwned = 200, CurrentPrice = 123.45M }.NetValue.Dump();
public abstract class Asset // Note abstract keyword
{
public abstract decimal NetValue { get; } // Note empty implementation
}
public class Stock : Asset
{
public long SharesOwned;
public decimal CurrentPrice;
// Override like a virtual method.
public override decimal NetValue => CurrentPrice * SharesOwned;
}
Hiding Inherited Members with new
// A base class and a subclass may define identical members. This usually happens by accident:
B b = new B();
b.Counter.Dump(); // 2
// Notice the non-virtual behavior in the code below:
A referenceConvertedB = b;
referenceConvertedB.Counter.Dump(); // 1
public class A { public int Counter = 1; }
public class B : A { public int Counter = 2; }
// Occasionally, you want to hide a member deliberately, in which case you can apply the new
// modifier to the member in the subclass, to avoid the compiler warning. The behavior is the same:
public class X { public int Counter = 1; }
public class Y : X { public new int Counter = 2; }
new vs virtual
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // Overrider.Foo
b1.Foo(); // Overrider.Foo
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); // Hider.Foo
b2.Foo(); // BaseClass.Foo
public class BaseClass
{
public virtual void Foo() { Console.WriteLine ("BaseClass.Foo"); }
}
public class Overrider : BaseClass
{
public override void Foo() { Console.WriteLine ("Overrider.Foo"); }
}
public class Hider : BaseClass
{
public new void Foo() { Console.WriteLine ("Hider.Foo"); }
}
Sealing Functions & Classes
// An overridden function member may seal its implementation with the sealed keyword to prevent it
// from being overridden by further subclasses:
House mansion = new House { Name="McMansion", Mortgage=250000 };
Console.WriteLine (mansion.Liability); // 250000
public class Asset
{
public string Name;
public virtual decimal Liability => 0; // Virtual
}
public class House : Asset
{
public decimal Mortgage;
public sealed override decimal Liability => Mortgage; // Overridden + sealed
}
// You can also seal the class itself, implicitly sealing all the virtual functions:
public sealed class Stock : Asset { /* ... */ }
Constructors & Inheritance
// A subclass must declare its own constructors. In doing so, it can call any of the
// base class’s constructors with the base keyword:
new Subclass (123);
public class Baseclass
{
public int X;
public Baseclass () { }
public Baseclass (int x) { this.X = x; }
}
public class Subclass : Baseclass
{
public Subclass (int x) : base (x) { }
}
Implicit Calling of the Parameterless Base Class Constructor
// If a constructor in a subclass omits the base keyword, the base type’s parameterless
// constructor is implicitly called:
new Subclass();
public class BaseClass
{
public int X;
public BaseClass() { X = 1; }
}
public class Subclass : BaseClass
{
public Subclass() { Console.WriteLine (X); } // 1
}
Required members
// If a constructor in a subclass omits the base keyword, the base type’s parameterless
// constructor is implicitly called:
new Subclass();
public class BaseClass
{
public int X;
public BaseClass() { X = 1; }
}
public class Subclass : BaseClass
{
public Subclass() { Console.WriteLine (X); } // 1
}
Required members - adding constructors
// Should you wish to also write a constructor, you can apply the [SetsRequiredMembers]
// attribute to bypass the required member restriction for that constructor:
// Consumers can now benefit from the convenience of that constructor without any tradeoff:
Asset a1 = new Asset { Name = "House" }; // OK
Asset a2 = new Asset ("House"); // OK
Asset a3 = new Asset(); // Error!
// Notice that we also defined a parameterless constructor (for use with the object initializer).
// Its presence also ensures that subclasses remain under no burden to reproduce any constructor.
// In the following example, the House class chooses not to implement a convenience constructor:
House h1 = new House { Name = "House" }; // OK
House h2 = new House(); // Error!
Asset a4 = new Asset { Name = "House" }; // OK
Asset a5 = new Asset(); // Error: will not compile!
public class House : Asset { } // No constructor, no worries!
public class Asset
{
public required string Name;
public Asset() { }
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public Asset (string n) => Name = n;
}
Required members - constructors and subclassing
// In the following example, the House class chooses not to implement a convenience constructor:
House h1 = new House { Name = "House" }; // OK
House h2 = new House(); // Error!
Asset a1 = new Asset { Name = "House" }; // OK
Asset a2 = new Asset(); // Error: will not compile!
public class Asset
{
public required string Name;
public Asset() { }
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public Asset (string n) => Name = n;
}
public class House : Asset { } // No constructor, no worries!
Inheritance with primary constructors
// Classes with primary constructors can subclass with the following syntax:
public class BaseClass (int x) { /* ... */ }
public class Subclass (int x, int y) : BaseClass (x) { /* ... */ }
// The call to BaseClass(x) is equivalent to calling base(x) in the following example:
public class Subclass2 : BaseClass
{
public Subclass2 (int x, int y) : base (x) { /* ... */ }
}
Overloading and Resolution
// When calling an overload method, the method with the most specific
// parameter type match has precedence, based on the *compile-time* variable type:
void Main()
{
Foo (new House()); // Calls Foo (House)
Asset a = new House();
Foo (a); // Calls Foo (Asset)
}
static void Foo (Asset a) { "Foo Asset".Dump(); }
static void Foo (House h) { "Foo House".Dump(); }
public class Asset
{
public string Name;
}
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
The object Type
The object Type
// object (System.Object) is the ultimate base class for all types. Any type can be
// implicitly converted to object; we can leverage this to write a general-purpose Stack:
Stack stack = new Stack();
stack.Push ("sausage");
string s = (string)stack.Pop(); // Downcast, so explicit cast is needed
Console.WriteLine (s); // sausage
// You can even push value types:
stack.Push (3);
int three = (int)stack.Pop();
public class Stack
{
int position;
object[] data = new object [10];
public void Push (object obj) { data [position++] = obj; }
public object Pop() { return data [--position]; }
}
// Because Stack works with the object type, we can Push and Pop instances of any type
// to and from the Stack:
Boxing & Unboxing
// Boxing is the act of casting a value-type instance to a reference-type instance;
// unboxing is the reverse.
int x = 9;
object obj = x; // Box the int
int y = (int)obj; // Unbox the int
y.Dump();
Unboxing to Wrong Type
// When unboxing, the types must match exactly:
object obj = 9; // 9 is inferred to be of type int
long x = (long) obj; // InvalidCastException
Unboxing to Wrong Type - Fix
object obj = 9;
// First, unbox to the correct type (int), then implicitly convert to long:
long x = (int) obj;
x.Dump();
// This also works:
object obj2 = 3.5; // 3.5 is inferred to be of type double
int y = (int) (double) obj2; // x is now 3
y.Dump();
Copying Semantics of Boxing & Unboxing
// Boxing copies the value-type instance into the new object, and unboxing copies
// the contents of the object back into a value-type instance.
int i = 3;
object boxed = i;
i = 5;
Console.WriteLine (boxed); // 3
GetType and typeof
// All types in C# are represented at runtime with an instance of System.Type.
// There are two basic ways to get a System.Type object:
// • Call GetType on the instance.
// • Use the typeof operator on a type name.
Point p = new Point();
Console.WriteLine (p.GetType().Name); // Point
Console.WriteLine (typeof (Point).Name); // Point
Console.WriteLine (p.GetType() == typeof (Point)); // True
Console.WriteLine (p.X.GetType().Name); // Int32
Console.WriteLine (p.Y.GetType().FullName); // System.Int32
public class Point { public int X, Y; }
The ToString Method
// The ToString method is defined on System.Object and returns the default textual representation
// of a type instance:
// You can override the ToString method on custom types:
int x = 1;
string s = x.ToString(); // s is "1"
Panda p = new Panda { Name = "Petey" };
Console.WriteLine (p.ToString()); // Petey
public class Panda
{
public string Name;
public override string ToString() { return Name; }
}
Structs
Structs
// A struct is similar to a class, with several key differences (as described in the book).
// In particular, a struct is a value type rather than a reference type.
// The construction semantics are different, too:
Point p1 = new Point (); // p1.x and p1.y will be 1
Point p2 = new Point (2, 2); // p2.x and p2.y will be 2
p1.Dump();
p2.Dump();
struct Point
{
public int x, y;
public Point () { this.x = 1; this.y = 1; }
public Point (int x, int y) { this.x = x; this.y = y; }
}
Structs - Illegal Construction Examples
// Changing the following struct to a class makes the type legal:
Point p1 = new Point ();
Point p2 = new Point (2, 2);
Point p3 = new Point (2);
p1.Dump();
p2.Dump();
p3.Dump();
struct Point
{
public int x, y;
public Point () { this.x = 1; this.y = 1; }
public Point (int x, int y) { this.x = x; this.y = y; }
public Point (int x) { this.x = x; } // Illegal: y not assigned
}
Structs - default constructor
Point p1 = new Point(); // p1.x and p1.y will be 1
Point p2 = default; // p2.x and p2.y will be 0
var points = new Point [10]; // Each point in the array will be (0,0)
var test = new Test(); // test.p will be (0,0);
p1.Dump();
p2.Dump();
points.Dump();
test.Dump();
struct Point
{
public int x = 1;
public int y;
public Point() => y = 1;
}
class Test { public Point p; }
ref Structs
var points = new Point [100]; // Error: will not compile!
ref struct Point { public int X, Y; }
class MyClass { Point P; } // Error: will not compile!
Access Modifiers
Access Modifiers - Examples
// The access modifiers are public, internal, protected and private.
//
// public is the default for members of an enum or interface.
// internal is the default for nonnested types.
// private is the default for everything else.
class Class1 {} // Class1 is internal (default) - visible to other types in same assembly
public class Class2 {} // Class2 is visible to everything, including types in other assemblies
class ClassA
{
int x; // x is private (default) - cannot be accessed from other types
}
class ClassB
{
internal int x; // x can be accessed from other types in same assembly
}
class BaseClass
{
void Foo() {} // Foo is private (default)
protected void Bar() {} // Foo is accessible to subclasses
}
class Subclass : BaseClass
{
void Test1() { Foo(); } // Error - cannot access Foo
void Test2() { Bar(); } // OK
}
Friend Assemblies
// Unsigned friend:
// [assembly: InternalsVisibleTo ("Friend")]
// Signed friend:
// [assembly: InternalsVisibleTo ("StrongFriend, PublicKey=0024f000048c...")]
// To obtain an assembly's public key, hit F5 to run the following code:
using (var dialog = new OpenFileDialog())
{
dialog.Title = "Locate assembly";
dialog.Filter = "Assembly files|*.dll;*.exe";
dialog.DefaultExt = ".dll";
if (dialog.ShowDialog() != DialogResult.OK) return;
if (!File.Exists (dialog.FileName)) return;
var aName = Assembly.LoadFile (dialog.FileName).GetName();
string key = string.Join ("",
aName.GetPublicKey().Select (b => b.ToString ("x2")).ToArray());
string assemAttrib = "[assembly: InternalsVisibleTo (\""
+ aName.Name
+ ", PublicKey=" + key.Dump ("Full Key")
+ "\")]";
assemAttrib.Dump ("Assembly Attribute");
Clipboard.SetText (assemAttrib);
}
Accessibility Capping
// A type caps the accessibility of its declared members:
class C // Class C is implicitly internal
{
public void Foo() {} // Foo's accessibility is capped at internal
}
void Main() { }
Restrictions on Access Modifiers
// When overriding a base class function, accessibility must be identical on the overridden function:
class BaseClass { protected virtual void Foo() {} }
class Subclass1 : BaseClass { protected override void Foo() {} } // OK
class Subclass2 : BaseClass { public override void Foo() {} } // Error
// A subclass itself can be less accessible than a base class, but not more:
internal class A { }
public class B : A { } // Error
Interfaces
Interfaces
// The IEnumerator interface is part of the .NET Framework, defined in System.Collections.
// We can define our own version of this as follows:
IEnumerator e = new Countdown();
while (e.MoveNext())
Console.Write (e.Current); // 109876543210
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
// Here's a class that implements this interface:
class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext () => count-- > 0;
public object Current => count;
public void Reset() { throw new NotSupportedException(); }
}
Extending an Interface
// We can extend interfaces - just like extending classes:
IRedoable r = null;
IUndoable u = r;
public interface IUndoable { void Undo(); }
public interface IRedoable : IUndoable { void Redo(); }
Explicit Interface Implementation
// Implementing multiple interfaces can sometimes result in a collision between member signatures.
// You can resolve such collisions by explicitly implementing an interface member:
Widget w = new Widget();
w.Foo(); // Widget's implementation of I1.Foo
((I1)w).Foo(); // Widget's implementation of I1.Foo
((I2)w).Foo(); // Widget's implementation of I2.Foo
interface I1 { void Foo(); }
interface I2 { int Foo(); }
public class Widget : I1, I2
{
public void Foo ()
{
Console.WriteLine ("Widget's implementation of I1.Foo");
}
int I2.Foo()
{
Console.WriteLine ("Widget's implementation of I2.Foo");
return 42;
}
}
// Another reason to explicitly implement interface members is to hide members that are
// highly specialized and distracting to a type’s normal use case.
Implementing Interface Members Virtually
// An implicitly implemented interface member is, by default, sealed. It must be marked
// virtual or abstract in the base class in order to be overridden:
// Calling the interface member through either the base class or the interface
// calls the subclass’s implementation:
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // RichTextBox.Undo
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
Reimplementing an Interface in a Subclass
// A subclass can reimplement any interface member already implemented by a base class.
// Reimplementation hijacks a member implementation (when called through the interface):
// Calling the reimplemented member through the interface calls the subclass’s implementation:
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
void IUndoable.Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable
{
public new void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
Reimplementing an Interface - Contrast
// Suppose that TextBox instead implemented Undo implicitly:
// This would give us another way to call Undo, which would “break” the system, as shown in Case 3:
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
((TextBox)r).Undo(); // TextBox.Undo Case 3
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable
{
public new void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
Alternatives to interface reimplementation
// Even with explicit member implementation, interface reimplementation is problematic for a
// couple of reasons.
// The following pattern is a good alternative if you need explicit interface implementation:
IUndoable r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
void IUndoable.Undo() => Undo(); // Calls method below
protected virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
Interfaces and Boxing
// Casting a struct to an interface causes boxing. Calling an implicitly implemented
// member on a struct does not cause boxing:
S s = new S();
s.Foo(); // No boxing.
I i = s; // Box occurs when casting to interface.
i.Foo();
interface I { void Foo(); }
struct S : I { public void Foo() {} }
Default interface members
var logger = new Logger();
// We can't call the Log method directly:
// foo.Log ("message") // Won't compile
// But we can call it via the interface:
((ILogger)logger).Log ("message");
interface ILogger
{
void Log (string text) => Console.WriteLine (text);
}
class Logger : ILogger
{
// We don't need to implement anything
}
Static nonvirtual interface members
ILogger.Prefix = "File log: ";
var logger = new Logger();
((ILogger)logger).Log ("message");
interface ILogger
{
void Log (string text) =>
Console.WriteLine (Prefix + text);
static string Prefix = "";
}
class Logger : ILogger
{
// We don't need to implement anything
}
Static nonvirtual interface members - scenario
ILogger foo = new Logger();
foo.Log (new Exception ("test"));
class Logger : ILogger
{
public void Log (string message) => Console.WriteLine (message);
}
interface ILogger
{
// Let's suppose the interface as always defined this method:
void Log (string message);
// Adding a new member to an interface need not break implementors:
public void Log (Exception ex) => Log (ExceptionHeader + ex.Message);
static string ExceptionHeader = "Exception: ";
}
Static virtual & abstract interface members
// Static virtual/abstract interface members (from C# 11) enable static polymorphism,
// an advanced feature that we will discuss in Chapter 4
interface ITypeDescribable
{
static abstract string Description { get; }
static virtual string Category => null;
}
// An implementing class or struct must implement static abstract members, and can
// optionally implement static virtual members
class CustomerTest : ITypeDescribable
{
public static string Description => "Customer tests"; // Mandatory
public static string Category => "Unit testing"; // Optional
}
Enums
Enums
// An enum is a special value type that lets you specify a group of named numeric constants:
BorderSide topSide = BorderSide.Top;
bool isTop = (topSide == BorderSide.Top);
isTop.Dump();
public enum BorderSide { Left, Right, Top, Bottom }
// You may specify an alternative integral type:
public enum BorderSideByte : byte { Left, Right, Top, Bottom }
// You may also specify an explicit underlying value for each enum member:
public enum BorderSideExplicit : byte { Left=1, Right=2, Top=10, Bottom=11 }
public enum BorderSidePartiallyExplicit : byte { Left=1, Right, Top=10, Bottom }
Enum Conversions
// You can convert an enum instance to and from its underlying integral value with an explicit cast:
int i = (int)BorderSide.Left;
i.Dump ("i");
BorderSide side = (BorderSide)i;
side.Dump ("side");
bool leftOrRight = (int)side <= 2;
leftOrRight.Dump ("leftOrRight");
HorizontalAlignment h = (HorizontalAlignment)BorderSide.Right;
h.Dump ("h");
BorderSide b = 0; // No cast required with the 0 literal.
b.Dump ("b");
public enum BorderSide { Left, Right, Top, Bottom }
public enum HorizontalAlignment
{
Left = BorderSide.Left,
Right = BorderSide.Right,
Center
}
Flags Enums
// You can combine enum members. To prevent ambiguities, members of a combinable enum require
// explicitly assigned values, typically in powers of two:
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
if ((leftRight & BorderSides.Left) != 0)
Console.WriteLine ("Includes Left"); // Includes Left
string formatted = leftRight.ToString(); // "Left, Right"
BorderSides s = BorderSides.Left;
s |= BorderSides.Right;
Console.WriteLine (s == leftRight); // True
s ^= BorderSides.Right; // Toggles BorderSides.Right
Console.WriteLine (s); // Left
[Flags]
public enum BorderSides { None = 0, Left = 1, Right = 2, Top = 4, Bottom = 8 }
Flags Enums - Combinations
// For convenience, you can include combination members within an enum declaration itself:
BorderSides.All.Dump();
// The bitwise, arithmetic, and comparison operators return the result of processing
// the underlying integral values:
(BorderSides.All ^ BorderSides.LeftRight).Dump();
[Flags]
public enum BorderSides
{
None = 0,
Left = 1, Right = 2, Top = 4, Bottom = 8,
LeftRight = Left | Right,
TopBottom = Top | Bottom,
All = LeftRight | TopBottom
}
Type-Safety Issues
// Since an enum can be cast to and from its underlying integral type, the actual value
// it may have may fall outside the bounds of a legal enum member:
BorderSide b = (BorderSide)12345;
Console.WriteLine (b); // 12345
BorderSide b2 = BorderSide.Bottom;
b2++; // No errors
Console.WriteLine (b2); // 4 (illegal value)
// An invalid BorderSide would break the following method:
void Draw (BorderSide side)
{
if (side == BorderSide.Left) { /*...*/ }
else if (side == BorderSide.Right) { /*...*/ }
else if (side == BorderSide.Top) { /*...*/ }
else { /*...*/ } // Assume BorderSide.Bottom
}
public enum BorderSide { Left, Right, Top, Bottom }
Type-Safety Issues - Workaround
for (int i = 0; i <= 16; i++)
{
BorderSides side = (BorderSides)i;
Console.WriteLine (IsFlagDefined (side) + " " + side);
}
bool IsFlagDefined (Enum e)
{
decimal d;
return !decimal.TryParse (e.ToString(), out d);
}
[Flags]
public enum BorderSides { Left = 1, Right = 2, Top = 4, Bottom = 8 }
Nested Types
Nested Types
// A nested type is declared within the scope of another type. For example:
public class TopLevel
{
public class Nested { } // Nested class
public enum Color { Red, Blue, Tan } // Nested enum
}
static void Main()
{
TopLevel.Color color = TopLevel.Color.Red;
}
Nested Types - Private Member Visibility
public class TopLevel
{
static int x;
public class Nested
{
public static void Foo() { Console.WriteLine (TopLevel.x); }
}
}
static void Main()
{
TopLevel.Nested.Foo();
}
Nested Types - Protected Member Visibility
public class TopLevel
{
protected class Nested { }
}
public class SubTopLevel : TopLevel
{
static void Foo() { new TopLevel.Nested(); }
}
static void Main()
{
}
Generics
Generic Types
// A generic type declares type parameters—placeholder types to be filled in by the consumer
// of the generic type, which supplies the type arguments:
var stack = new Stack<int>();
stack.Push (5);
stack.Push (10);
int x = stack.Pop(); // x is 10
int y = stack.Pop(); // y is 5
x.Dump(); y.Dump();
public class Stack<T>
{
int position;
T[] data = new T [100];
public void Push (T obj) => data [position++] = obj;
public T Pop() => data [--position];
}
Why Generics Exist
// Generics exist to write code that is reusable across different types. Without generic types,
// writing a general-purpose stack would require a solution such as this:
// Now suppose we want a stack that stores just integers:
ObjectStack stack = new ObjectStack();
// It's easy to make mistakes:
stack.Push ("s"); // Wrong type, but no error!
int i = (int)stack.Pop(); // Downcast - runtime error!
public class ObjectStack
{
int position;
object[] data = new object [10];
public void Push (object obj) => data [position++] = obj;
public object Pop() => data [--position];
}
Generic Methods
// A generic method declares type parameters within the signature of a method.
int x = 5;
int y = 10;
Swap (ref x, ref y);
x.Dump(); y.Dump();
static void Swap<T> (ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
Declaring Type Parameters
// Type parameters can be introduced in the declaration of classes, structs, interfaces,
// delegates (covered in Chapter 4), and methods:
// To instantiate:
Dictionary<int, string> myDic = new Dictionary<int, string>();
// Or:
var myDicEasy = new Dictionary<int, string>();
struct Nullable<T>
{
public T Value { get; set; }
}
// A generic type or method can have multiple parameters:
class Dictionary<TKey, TValue> { /*...*/ }
// Generic type names and method names can be overloaded as long as the number of type
// parameters is different:
class A { }
class A<T> { }
class A<T1, T2> { }
Typeof and Unbound Generic Types
// It's possible for an unbound generic type to exist at runtime—purely as a Type object.
// The only way to specify an unbound generic type in C# is with the typeof operator:
Type a1 = typeof (A<>); // Unbound type (notice no type arguments).
Type a2 = typeof (A<,>); // Use commas to indicate multiple type args.
// You can also use the typeof operator to specify a closed type:
Type a3 = typeof (A<int, int>);
class A<T> { }
class A<T1, T2> { }
// or an open type (which is closed at runtime):
class B<T>
{
void X() { Type t = typeof (T); }
}
The default Generic Value
// The default keyword can be used to get the default value given a generic type parameter:
int[] numbers = { 1, 2, 3 };
Zap (numbers);
numbers.Dump();
static void Zap<T> (T[] array)
{
for (int i = 0; i < array.Length; i++)
array [i] = default (T);
}
Generic Constraints
/* Constraints can be applied to a type parameter restrict the type arguments.
where T : base-class // Base class constraint
where T : interface // Interface constraint
where T : class // Reference-type constraint
where T : struct // Value-type constraint (excludes Nullable types)
where T : new() // Parameterless constructor constraint
where U : T // Naked type constraint
*/
int z = Max (5, 10); // 10
string last = Max ("ant", "zoo"); // zoo
z.Dump(); last.Dump();
T Max<T> (T a, T b) where T : IComparable<T> // Self-referencing interface constraint
{
return a.CompareTo (b) > 0 ? a : b;
}
class SomeClass { }
interface Interface1 { }
class GenericClass<T> where T : SomeClass, Interface1 { } // Class & interface constraint
Parameterless Constructor Constraint
// The parameterless constructor constraint requires T to have a public parameterless constructor.
// If this constraint is defined, you can call new() on T:
var builders = new StringBuilder [100];
Initialize (builders);
builders [37].Dump();
static void Initialize<T> (T[] array) where T : new()
{
for (int i = 0; i < array.Length; i++)
array [i] = new T();
}
Naked Type Constraint
// The naked type constraint requires one type parameter to derive from another type parameter:
class Stack<T>
{
Stack<U> FilteredStack<U>() where U : T
{
/* ... */
return default(Stack<U>);
}
}
Subclassing Generic Typest
// A generic class can be subclassed just like a nongeneric class.
// The subclass can leave the base class’s type parameters open:
class Stack<T> { /*...*/ }
class SpecialStack<T> : Stack<T> { /*...*/ }
// Or the subclass can close the generic type parameters with a concrete type:
class IntStack : Stack<int> { /*...*/ }
// A subtype can also introduce fresh type arguments:
class List<T> { /*...*/ }
class KeyedList<T,TKey> : List<T> { /*...*/ }
Self-Referencing Generic Declarations
// A type can name itself as the concrete type when closing a type argument:
var b1 = new Balloon { Color = "Red", CC = 123 };
var b2 = new Balloon { Color = "Red", CC = 123 };
b1.Equals (b2).Dump();
public class Balloon : IEquatable<Balloon>
{
public string Color { get; set; }
public int CC { get; set; }
public bool Equals (Balloon b)
{
if (b == null) return false;
return b.Color == Color && b.CC == CC;
}
// In real life, we would override object.Equals / GetHashCode as well - see Chapter 6.
}
Static Data
// Static data is unique for each closed type:
Console.WriteLine (++Bob<int>.Count); // 1
Console.WriteLine (++Bob<int>.Count); // 2
Console.WriteLine (++Bob<string>.Count); // 1
Console.WriteLine (++Bob<object>.Count); // 1
class Bob<T> { public static int Count; }
Type Parameters & Conversions - Problem
// The most common scenario is when you want to perform a reference conversion:
StringBuilder Foo<T> (T arg)
{
if (arg is StringBuilder)
return (StringBuilder) arg; // Will not compile: Cannot convert T to StringBuilder
/*...*/
return null;
}
Type Parameters & Conversions - Solution #1
// The simplest solution is to instead use the as operator, which is unambiguous because
// it cannot perform custom conversions:
StringBuilder Foo<T> (T arg)
{
StringBuilder sb = arg as StringBuilder;
if (sb != null) return sb;
/*...*/
return null;
}
Type Parameters & Conversions - Solution #2
// A more general solution is to first cast to object:
StringBuilder Foo<T> (T arg)
{
if (arg is StringBuilder)
return (StringBuilder) (object) arg;
/*...*/
return null;
}
Type Parameters & Conversions - Unboxing
// Unboxing conversions can also introduce ambiguities; again the solution is to first cast to object:
int Foo<T> (T x) => (int) (object) x;
Covariance - Classes - Problem
// Generic classes are not covariant, to ensure static type safety. Consider the following:
// The following fails to compile:
Stack<Bear> bears = new Stack<Bear>();
Stack<Animal> animals = bears; // Compile-time error
// That restriction prevents the possibility of runtime failure with the following code:
animals.Push (new Camel()); // Trying to add Camel to bears
class Animal { }
class Bear : Animal { }
class Camel : Animal { }
public class Stack<T> // A simple Stack implementation
{
int position;
T[] data = new T [100];
public void Push (T obj) => data [position++] = obj;
public T Pop() => data [--position];
}
Covariance - Classes - Hindering Reusability
// Lack of covariance with classes can hinder reusability.
Stack<Bear> bears = new Stack<Bear>();
ZooCleaner.Wash (bears); // Will not compile!
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack<T> // A simple Stack implementation
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data [--position];
}
static class ZooCleaner
{
public static void Wash (Stack<Animal> animals) { /*...*/ }
}
Covariance - Classes - Workaround
// Lack of covariance with classes can hinder reusability.
Stack<Bear> bears = new Stack<Bear>();
ZooCleaner.Wash (bears); // Works!
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack<T> // A simple Stack implementation
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
static class ZooCleaner
{
public static void Wash<T>(Stack<T> animals) where T : Animal { /*...*/ }
}
Covariance - Arrays
// For historical reasons, array types are covariant.
Bear[] bears = new Bear[3];
Animal[] animals = bears; // OK
// The downside of this reusability is that element assignments can fail at runtime:
animals[0] = new Camel(); // Runtime error
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
Covariance - Interfaces
// As of C# 4.0, generic interfaces support covariance for type parameters marked with the out modifier:
var bears = new Stack<Bear>();
bears.Push (new Bear());
// Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>:
IPoppable<Animal> animals = bears; // Legal
Animal a = animals.Pop();
public interface IPoppable<out T> { T Pop(); }
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack<T> : IPoppable<T>
{
int position;
T[] data = new T [100];
public void Push (T obj) => data [position++] = obj;
public T Pop() => data [--position];
}
// This is also now legal:
class ZooCleaner
{
public static void Wash (IPoppable<Animal> animals) { /*...*/ }
}
Contravariance - Interfaces
// Type parameters marked with the in modifier indicate contravariance:
IPushable<Animal> animals = new Stack<Animal>();
IPushable<Bear> bears = animals; // Legal
bears.Push (new Bear());
public interface IPoppable<out T> { T Pop(); }
public interface IPushable<in T> { void Push (T obj); }
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
// Note that Stack<T> can implement both IPoppable<T> and IPushable<T>:
public class Stack<T> : IPoppable<T>, IPushable<T>
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
Contravariance - More Examples
/* The following interface is defined as part of the .NET Framework:
public interface IComparer<in T>
{
// Returns a value indicating the relative ordering of a and b
int Compare (T a, T b);
}
*/
var objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer;
int result = stringComparer.Compare ("Brett", "Jemaine");
result.Dump();