mirror of
https://github.com/markjprice/cs11dotnet7.git
synced 2025-12-06 05:32:03 +01:00
861 lines
15 KiB
Plaintext
861 lines
15 KiB
Plaintext
#!markdown
|
|
|
|
# Chapter 6 - Implementing Interfaces and Inheriting Classes
|
|
|
|
#!csharp
|
|
|
|
using static System.Console;
|
|
|
|
#!csharp
|
|
|
|
public class PersonException : Exception
|
|
{
|
|
public PersonException() : base() { }
|
|
|
|
public PersonException(string message) : base(message) { }
|
|
|
|
public PersonException(string message, Exception innerException)
|
|
: base(message, innerException) { }
|
|
}
|
|
|
|
#!csharp
|
|
|
|
#nullable enable // required in notebook cells that use nullability
|
|
|
|
public class Person : object, IComparable<Person>
|
|
{
|
|
// fields
|
|
public string? Name; // ? allows null
|
|
public DateTime DateOfBirth;
|
|
public List<Person> Children = new(); // C# 9 or later
|
|
|
|
// methods
|
|
public void WriteToConsole()
|
|
{
|
|
WriteLine($"{Name} was born on a {DateOfBirth:dddd}.");
|
|
}
|
|
|
|
// static method to "multiply"
|
|
public static Person Procreate(Person p1, Person p2)
|
|
{
|
|
Person baby = new()
|
|
{
|
|
Name = $"Baby of {p1.Name} and {p2.Name}"
|
|
};
|
|
|
|
p1.Children.Add(baby);
|
|
p2.Children.Add(baby);
|
|
|
|
return baby;
|
|
}
|
|
|
|
// instance method to "multiply"
|
|
public Person ProcreateWith(Person partner)
|
|
{
|
|
return Procreate(this, partner);
|
|
}
|
|
|
|
// operator to "multiply"
|
|
public static Person operator *(Person p1, Person p2)
|
|
{
|
|
return Person.Procreate(p1, p2);
|
|
}
|
|
|
|
// method with a local function
|
|
public static int Factorial(int number)
|
|
{
|
|
if (number < 0)
|
|
{
|
|
throw new ArgumentException(
|
|
$"{nameof(number)} cannot be less than zero.");
|
|
}
|
|
return localFactorial(number);
|
|
|
|
int localFactorial(int localNumber) // local function
|
|
{
|
|
if (localNumber < 1) return 1;
|
|
return localNumber * localFactorial(localNumber - 1);
|
|
}
|
|
}
|
|
|
|
// delegate field
|
|
public event EventHandler? Shout;
|
|
|
|
// data field
|
|
public int AngerLevel;
|
|
|
|
// method
|
|
public void Poke()
|
|
{
|
|
AngerLevel++;
|
|
|
|
if (AngerLevel >= 3)
|
|
{
|
|
// if something is listening...
|
|
if (Shout != null)
|
|
{
|
|
// ...then call the delegate
|
|
Shout(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int CompareTo(Person? other)
|
|
{
|
|
if (Name is null) return 0;
|
|
return Name.CompareTo(other?.Name);
|
|
}
|
|
|
|
// overridden methods
|
|
public override string ToString()
|
|
{
|
|
return $"{Name} is a {base.ToString()}";
|
|
}
|
|
|
|
public void TimeTravel(DateTime when)
|
|
{
|
|
if (when <= DateOfBirth)
|
|
{
|
|
throw new PersonException("If you travel back in time to a date earlier than your own birth, then the universe will explode!");
|
|
}
|
|
else
|
|
{
|
|
WriteLine($"Welcome to {when:yyyy}!");
|
|
}
|
|
}
|
|
|
|
// end of class
|
|
}
|
|
|
|
#!markdown
|
|
|
|
# Implementing functionality using methods
|
|
|
|
#!csharp
|
|
|
|
Person harry = new() { Name = "Harry" };
|
|
Person mary = new() { Name = "Mary" };
|
|
Person jill = new() { Name = "Jill" };
|
|
|
|
// call instance method
|
|
Person baby1 = mary.ProcreateWith(harry);
|
|
baby1.Name = "Gary";
|
|
|
|
// call static method
|
|
Person baby2 = Person.Procreate(harry, jill);
|
|
|
|
// call an operator
|
|
Person baby3 = harry * mary;
|
|
|
|
WriteLine($"{harry.Name} has {harry.Children.Count} children.");
|
|
WriteLine($"{mary.Name} has {mary.Children.Count} children.");
|
|
WriteLine($"{jill.Name} has {jill.Children.Count} children.");
|
|
WriteLine(
|
|
format: "{0}'s first child is named \"{1}\".",
|
|
arg0: harry.Name,
|
|
arg1: harry.Children[0].Name);
|
|
|
|
#!markdown
|
|
|
|
## Implementing functionality using local functions
|
|
|
|
#!csharp
|
|
|
|
WriteLine($"5! is {Person.Factorial(5)}");
|
|
|
|
#!markdown
|
|
|
|
# Raising and handling events
|
|
|
|
#!markdown
|
|
|
|
## Defining and handling delegates
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
static void Harry_Shout(object? sender, EventArgs e)
|
|
{
|
|
if (sender is null) return;
|
|
Person p = (Person)sender;
|
|
WriteLine($"{p.Name} is this angry: {p.AngerLevel}.");
|
|
}
|
|
|
|
#!csharp
|
|
|
|
harry.Shout += Harry_Shout;
|
|
|
|
#!csharp
|
|
|
|
harry.Poke();
|
|
harry.Poke();
|
|
harry.Poke();
|
|
harry.Poke();
|
|
|
|
#!markdown
|
|
|
|
# Making types safely reusable with generics
|
|
|
|
#!markdown
|
|
|
|
## Working with non-generic types
|
|
|
|
#!csharp
|
|
|
|
// non-generic lookup collection
|
|
System.Collections.Hashtable lookupObject = new();
|
|
|
|
lookupObject.Add(key: 1, value: "Alpha");
|
|
lookupObject.Add(key: 2, value: "Beta");
|
|
lookupObject.Add(key: 3, value: "Gamma");
|
|
lookupObject.Add(key: harry, value: "Delta");
|
|
|
|
int key = 2; // lookup the value that has 2 as its key
|
|
WriteLine(format: "Key {0} has value: {1}",
|
|
arg0: key,
|
|
arg1: lookupObject[key]);
|
|
|
|
// lookup the value that has harry as its key
|
|
WriteLine(format: "Key {0} has value: {1}",
|
|
arg0: harry,
|
|
arg1: lookupObject[harry]);
|
|
|
|
#!markdown
|
|
|
|
## Working with generic types
|
|
|
|
#!csharp
|
|
|
|
// generic lookup collection
|
|
Dictionary<int, string> lookupIntString = new();
|
|
|
|
lookupIntString.Add(key: 1, value: "Alpha");
|
|
lookupIntString.Add(key: 2, value: "Beta");
|
|
lookupIntString.Add(key: 3, value: "Gamma");
|
|
lookupIntString.Add(key: 4, value: "Delta");
|
|
|
|
key = 3;
|
|
WriteLine(format: "Key {0} has value: {1}",
|
|
arg0: key,
|
|
arg1: lookupIntString[key]);
|
|
|
|
#!markdown
|
|
|
|
# Implementing interfaces
|
|
|
|
#!markdown
|
|
|
|
## Comparing objects when sorting
|
|
|
|
#!csharp
|
|
|
|
Person[] people =
|
|
{
|
|
new() { Name = "Simon" },
|
|
new() { Name = "Jenny" },
|
|
new() { Name = "Adam" },
|
|
new() { Name = "Richard" }
|
|
};
|
|
|
|
WriteLine("Initial list of people:");
|
|
foreach (Person p in people)
|
|
{
|
|
WriteLine($" {p.Name}");
|
|
}
|
|
|
|
WriteLine("Use Person's IComparable implementation to sort:");
|
|
Array.Sort(people);
|
|
foreach (Person p in people)
|
|
{
|
|
WriteLine($" {p.Name}");
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Comparing objects using a separate class
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
public class PersonComparer : IComparer<Person>
|
|
{
|
|
public int Compare(Person? x, Person? y)
|
|
{
|
|
if (x is null || y is null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Compare the Name lengths...
|
|
int result = x.Name.Length.CompareTo(y.Name.Length);
|
|
|
|
// ...if they are equal...
|
|
if (result == 0)
|
|
{
|
|
// ...then compare by the Names...
|
|
return x.Name.CompareTo(y.Name);
|
|
}
|
|
else // result will be -1 or 1
|
|
{
|
|
// ...otherwise compare by the lengths.
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
WriteLine("Use PersonComparer's IComparer implementation to sort:");
|
|
Array.Sort(people, new PersonComparer());
|
|
foreach (Person p in people)
|
|
{
|
|
WriteLine($" {p.Name}");
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Implicit and explicit interface implementations
|
|
|
|
#!csharp
|
|
|
|
public interface IGamePlayer
|
|
{
|
|
void Lose();
|
|
}
|
|
|
|
public interface IKeyHolder
|
|
{
|
|
void Lose();
|
|
}
|
|
|
|
public class Human : IGamePlayer, IKeyHolder
|
|
{
|
|
public void Lose() // implicit implementation
|
|
{
|
|
// implement losing a key
|
|
WriteLine("Lost my key!");
|
|
}
|
|
void IGamePlayer.Lose() // explicit implementation
|
|
{
|
|
// implement losing a game
|
|
WriteLine("Lost the game!");
|
|
}
|
|
}
|
|
|
|
// calling implicit and explicit implementations of Lose
|
|
Human p = new();
|
|
|
|
p.Lose(); // calls implicit implementation of losing a key
|
|
|
|
((IGamePlayer)p).Lose(); // calls explicit implementation of losing a game
|
|
|
|
IGamePlayer player = p as IGamePlayer;
|
|
|
|
player.Lose(); // calls explicit implementation of losing a game
|
|
|
|
#!markdown
|
|
|
|
## Defining interfaces with default implementations
|
|
|
|
Note that the code cell below executes successfully despite the `DvdPlayer` class not implementing `Stop`.
|
|
|
|
#!csharp
|
|
|
|
public interface IPlayable
|
|
{
|
|
void Play();
|
|
void Pause();
|
|
|
|
void Stop() // default interface implementation
|
|
{
|
|
WriteLine("Default implementation of Stop.");
|
|
}
|
|
}
|
|
|
|
public class DvdPlayer : IPlayable
|
|
{
|
|
public void Pause()
|
|
{
|
|
WriteLine("DVD player is pausing.");
|
|
}
|
|
public void Play()
|
|
{
|
|
WriteLine("DVD player is playing.");
|
|
}
|
|
}
|
|
|
|
#!markdown
|
|
|
|
# Managing memory with reference and value types
|
|
|
|
#!markdown
|
|
|
|
## How reference and value types are stored in memory
|
|
|
|
#!csharp
|
|
|
|
int number1 = 49;
|
|
long number2 = 12;
|
|
System.Drawing.Point location = new(x: 4, y: 5);
|
|
|
|
Person kevin = new() { Name = "Kevin",
|
|
DateOfBirth = new(year: 1988, month: 9, day: 23) };
|
|
|
|
Person sally;
|
|
|
|
#!markdown
|
|
|
|
## Equality of types
|
|
|
|
#!csharp
|
|
|
|
int a = 3;
|
|
int b = 3;
|
|
WriteLine($"a == b: {(a == b)}"); // true
|
|
|
|
#!csharp
|
|
|
|
Person2 a = new() { Name = "Kevin" };
|
|
Person2 b = new() { Name = "Kevin" };
|
|
WriteLine($"a == b: {(a == b)}"); // false
|
|
|
|
#!csharp
|
|
|
|
Person2 a = new() { Name = "Kevin" };
|
|
Person2 b = a;
|
|
WriteLine($"a == b: {(a == b)}"); // true
|
|
|
|
#!csharp
|
|
|
|
string a = "Kevin";
|
|
string b = "Kevin";
|
|
WriteLine($"a == b: {(a == b)}"); // true
|
|
|
|
#!markdown
|
|
|
|
# Defining struct types
|
|
|
|
#!csharp
|
|
|
|
public struct DisplacementVector
|
|
{
|
|
public int X;
|
|
public int Y;
|
|
|
|
public DisplacementVector(int initialX, int initialY)
|
|
{
|
|
X = initialX;
|
|
Y = initialY;
|
|
}
|
|
|
|
public static DisplacementVector operator +(
|
|
DisplacementVector vector1,
|
|
DisplacementVector vector2)
|
|
{
|
|
return new(
|
|
vector1.X + vector2.X,
|
|
vector1.Y + vector2.Y);
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
DisplacementVector dv1 = new(3, 5);
|
|
DisplacementVector dv2 = new(-2, 7);
|
|
DisplacementVector dv3 = dv1 + dv2;
|
|
|
|
WriteLine($"({dv1.X}, {dv1.Y}) + ({dv2.X}, {dv2.Y}) = ({dv3.X}, {dv3.Y})");
|
|
|
|
#!markdown
|
|
|
|
## Working with record struct types
|
|
|
|
#!csharp
|
|
|
|
public record struct DisplacementVector(int X, int Y);
|
|
|
|
#!markdown
|
|
|
|
## Releasing unmanaged resources
|
|
|
|
#!csharp
|
|
|
|
public class Animal
|
|
{
|
|
public Animal() // constructor
|
|
{
|
|
// allocate any unmanaged resources
|
|
}
|
|
|
|
~Animal() // Finalizer aka destructor
|
|
{
|
|
// deallocate any unmanaged resources
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
public class Animal : IDisposable
|
|
{
|
|
public Animal()
|
|
{
|
|
// allocate unmanaged resource
|
|
}
|
|
|
|
~Animal() // Finalizer
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
bool disposed = false; // have resources been released?
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
// tell garbage collector it does not need to call the finalizer
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposed) return;
|
|
|
|
// deallocate the *unmanaged* resource
|
|
// ...
|
|
|
|
if (disposing)
|
|
{
|
|
// deallocate any other *managed* resources
|
|
// ...
|
|
}
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
using (Animal a = new())
|
|
{
|
|
// code that uses the Animal instance
|
|
}
|
|
|
|
#!csharp
|
|
|
|
Animal a = new();
|
|
try
|
|
{
|
|
// code that uses the Animal instance
|
|
}
|
|
finally
|
|
{
|
|
if (a != null) a.Dispose();
|
|
}
|
|
|
|
#!markdown
|
|
|
|
# Working with null values
|
|
|
|
#!csharp
|
|
|
|
int thisCannotBeNull = 4;
|
|
//thisCannotBeNull = null; // compile error!
|
|
|
|
int? thisCouldBeNull = null;
|
|
WriteLine(thisCouldBeNull);
|
|
WriteLine(thisCouldBeNull.GetValueOrDefault());
|
|
|
|
thisCouldBeNull = 7;
|
|
WriteLine(thisCouldBeNull);
|
|
WriteLine(thisCouldBeNull.GetValueOrDefault());
|
|
|
|
#!markdown
|
|
|
|
## Declaring non-nullable variables and parameters
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
class Address
|
|
{
|
|
public string? Building;
|
|
public string Street = string.Empty;
|
|
public string City = string.Empty;
|
|
public string Region = string.Empty;
|
|
}
|
|
|
|
Address address = new();
|
|
address.Building = null;
|
|
address.Street = null;
|
|
address.City = "London";
|
|
address.Region = null;
|
|
|
|
#!markdown
|
|
|
|
## Checking for null
|
|
|
|
#!csharp
|
|
|
|
string authorName = null;
|
|
|
|
// the following throws a NullReferenceException
|
|
//int x = authorName.Length;
|
|
|
|
// instead of throwing an exception, null is assigned to y
|
|
int? y = authorName?.Length;
|
|
|
|
if (!y.HasValue) WriteLine("y is null");
|
|
|
|
// result will be 3 if authorName?.Length is null
|
|
int result = authorName?.Length ?? 3;
|
|
Console.WriteLine(result);
|
|
|
|
#!markdown
|
|
|
|
# Inheriting from classes
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
public class Employee : Person
|
|
{
|
|
public string? EmployeeCode { get; set; }
|
|
public DateTime HireDate { get; set; }
|
|
|
|
public new void WriteToConsole()
|
|
{
|
|
WriteLine(format:
|
|
"{0} was born on {1:dd/MM/yy} and hired on {2:dd/MM/yy}",
|
|
arg0: Name,
|
|
arg1: DateOfBirth,
|
|
arg2: HireDate);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"{Name}'s code is {EmployeeCode}";
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
Employee john = new()
|
|
{
|
|
Name = "John Jones",
|
|
DateOfBirth = new(year: 1990, month: 7, day: 28)
|
|
};
|
|
john.WriteToConsole();
|
|
|
|
#!markdown
|
|
|
|
## Extending classes to add functionality
|
|
|
|
#!csharp
|
|
|
|
john.EmployeeCode = "JJ001";
|
|
john.HireDate = new(year: 2014, month: 11, day: 23);
|
|
WriteLine($"{john.Name} was hired on {john.HireDate:dd/MM/yy}");
|
|
|
|
#!markdown
|
|
|
|
## Overriding members
|
|
|
|
#!csharp
|
|
|
|
WriteLine(john.ToString());
|
|
|
|
#!markdown
|
|
|
|
## Inheriting from abstract classes
|
|
|
|
#!csharp
|
|
|
|
public interface INoImplementation // C# 1.0 and later
|
|
{
|
|
void Alpha(); // must be implemented by derived type
|
|
}
|
|
|
|
public interface ISomeImplementation // C# 8.0 and later
|
|
{
|
|
void Alpha(); // must be implemented by derived type
|
|
|
|
void Beta()
|
|
{
|
|
// default implementation; can be overridden
|
|
}
|
|
}
|
|
|
|
public abstract class PartiallyImplemented // C# 1.0 and later
|
|
{
|
|
public abstract void Gamma(); // must be implemented by derived type
|
|
|
|
public virtual void Delta() // can be overridden
|
|
{
|
|
// implementation
|
|
}
|
|
}
|
|
|
|
public class FullyImplemented : PartiallyImplemented, ISomeImplementation
|
|
{
|
|
public void Alpha()
|
|
{
|
|
// implementation
|
|
}
|
|
public override void Gamma()
|
|
{
|
|
// implementation
|
|
}
|
|
}
|
|
|
|
// you can only instantiate the fully implemented class
|
|
FullyImplemented a = new();
|
|
|
|
// all the other types give compile errors
|
|
PartiallyImplemented b = new(); // compile error!
|
|
ISomeImplementation c = new(); // compile error!
|
|
INoImplementation d = new(); // compile error!
|
|
|
|
#!markdown
|
|
|
|
## Preventing inheritance and overriding
|
|
|
|
You can prevent another developer from inheriting from your class by applying the `sealed`
|
|
keyword to its definition.
|
|
|
|
#!csharp
|
|
|
|
public sealed class ScroogeMcDuck
|
|
{
|
|
}
|
|
|
|
#!markdown
|
|
|
|
You can prevent someone from further overriding a `virtual` method in your class by applying
|
|
the `sealed` keyword to the method. You can only seal an overridden method.
|
|
|
|
#!csharp
|
|
|
|
public class Singer
|
|
{
|
|
// virtual allows this method to be overridden
|
|
public virtual void Sing()
|
|
{
|
|
WriteLine("Singing...");
|
|
}
|
|
}
|
|
|
|
public class LadyGaga : Singer
|
|
{
|
|
// sealed prevents overriding the method in subclasses
|
|
public sealed override void Sing()
|
|
{
|
|
WriteLine("Singing with style...");
|
|
}
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Understanding polymorphism
|
|
|
|
#!csharp
|
|
|
|
Employee aliceInEmployee = new()
|
|
{ Name = "Alice", EmployeeCode = "AA123" };
|
|
|
|
Person aliceInPerson = aliceInEmployee;
|
|
aliceInEmployee.WriteToConsole();
|
|
aliceInPerson.WriteToConsole();
|
|
WriteLine(aliceInEmployee.ToString());
|
|
WriteLine(aliceInPerson.ToString());
|
|
|
|
#!markdown
|
|
|
|
## Casting within inheritance hierarchies
|
|
|
|
#!csharp
|
|
|
|
Employee explicitAlice = aliceInPerson;
|
|
|
|
#!csharp
|
|
|
|
if (aliceInPerson is Employee)
|
|
{
|
|
WriteLine($"{nameof(aliceInPerson)} IS an Employee");
|
|
Employee explicitAlice = (Employee)aliceInPerson;
|
|
// safely do something with explicitAlice
|
|
}
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
Employee? aliceAsEmployee = aliceInPerson as Employee; // could be null
|
|
if (aliceAsEmployee != null)
|
|
{
|
|
WriteLine($"{nameof(aliceInPerson)} AS an Employee");
|
|
// safely do something with aliceAsEmployee
|
|
}
|
|
|
|
#!markdown
|
|
|
|
# Inheriting and extending .NET types
|
|
|
|
#!csharp
|
|
|
|
try
|
|
{
|
|
john.TimeTravel(when: new(1999, 12, 31));
|
|
john.TimeTravel(when: new(1950, 12, 25));
|
|
}
|
|
catch (PersonException ex)
|
|
{
|
|
WriteLine(ex.Message);
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Using static methods to reuse functionality
|
|
|
|
#!csharp
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
#!csharp
|
|
|
|
public class StringExtensions
|
|
{
|
|
public static bool IsValidEmail(string input)
|
|
{
|
|
// use simple regular expression to check
|
|
// that the input string is a valid email
|
|
return Regex.IsMatch(input,
|
|
@"[a-zA-Z0-9\.-_]+@[a-zA-Z0-9\.-_]+");
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
string email1 = "pamela@test.com";
|
|
string email2 = "ian&test.com";
|
|
|
|
WriteLine("{0} is a valid e-mail address: {1}",
|
|
arg0: email1,
|
|
arg1: StringExtensions.IsValidEmail(email1));
|
|
|
|
WriteLine("{0} is a valid e-mail address: {1}",
|
|
arg0: email2,
|
|
arg1: StringExtensions.IsValidEmail(email2));
|
|
|
|
#!markdown
|
|
|
|
Extension methods must be defined in a top-level static method so they cannot be defined in a notebook.
|
|
|
|
#!markdown
|
|
|
|
# Using an analyzer to write better code
|
|
|
|
You cannot run code analyzers on notebook code.
|