Here are notes about the topics we covered in lecture 5. For more details, see the Essential C# textbook or the C# reference pages.
An assignment is actually an expression in C#:
y = (x = 4) + 2; // assign 4 to x and 6 to y
Structs are similar to classes, but they are value types, not reference types. Here is a simple struct:
struct Point { public double x, y; public Point(double x, double y) { this.x = x; this.y = y; } public static double distance(Point p, Point q) => Sqrt((p.x - q.x) * 2 + (p.y - q.y) * 2); }
Structs have some limitations:
Fields in a struct may not have initializers.
A struct may not have a constructor with no arguments.
It is not possible to inherit from a struct.
It is sometimes more efficient to use structs than classes, since a variable of struct type does not contain an implicit pointer. On the other hand, each time a struct is passed as a method argument, it is copied, which is more expensive than passing a class reference by value. So in some situations structs are less efficient.
I generally recommend that you use structs only for data types that contain just a few values such as integers or doubles. For example, types that represent rational numbers or complex numbers would be good candidates for structs.
An interface defines a set of methods, properties and/or indexers that can be implemented by a class. For example:
interface Stack { bool isEmpty { get; } void push(int i); int pop(); }
A class may implement one or more interfaces:
class LinkedStack : Stack { Node head; public bool isEmpty { get => (head == null); } public void push(int i) { head = new Node(i, head); } public int pop() { ... } }
An interface may inherit from one or more other interfaces.
For example, we can refactor the Stack
interface above
as follows:
interface Collection { bool isEmpty { get; } } interface Stack : Collection { void push(int i); int pop(); } interface Queue : Collection { void enqueue(int i); int dequeue(); }
In some situations a class may be a subtype of another type:
when a class implements an interface
when an interface inherits from an interface
when a class inherits from a class (see below)
For example:
interface Collection { … } interface Stack : Collection { … } class SimpleStack : Stack { … } class HyperStack : SimpleStack { … }
Here, HyperStack
is a subtype of SimpleStack
,
which is a subtype of Stack
, which is a subtype of
Collection
.
A type may be implicitly converted to any supertype:
Collection c = new HyperStack(); // implicit conversion from HyperStack to Collection
A type may be explicitly converted to any subtype:
HyperStack h = (HyperStack) c; // explicit conversion from Collection to HyperStack
This explicit conversion will fail at runtime if the object in question does not actually belong to the subtype.
Usually a class implements interface methods (and other members)
implicitly, as in the LinkedStack example above. An implicit
member implementation includes the keyword public
and is
visible through variables of either the class type or the interface
type:
LinkedStack ls = new LinkedStack(); ls.push(3); Stack s = ls; s.push(4);
Alternatively, a class may implement a member explicitly. An
explicit member implementation is prefixed with the interface name
and may not be public
:
class AlterStack : Stack { void Stack.push(int i) { … } … }
An explicit member implementation is visbile only through a variable of the interface type, not the class type:
AlterStack a = new AlterStack(); a.push(3); // invalid - compile error Stack b = new AlterStack(); b.push(3); // works fine
Explicit member implementations are useful when a class implements two interfaces and a member with the same name occurs in both interfaces, i.e. when there is a name conflict. In this situation, a class can provide a separate explicit implementation for each interface. But we will not often (if ever) encounter this situation in this course.
The is
operator returns true if a value belongs to a
type. It works with both nullable types and reference types:
int? i = abc(); if (i is int) // true if i != null WriteLine(i.Value); Stack s = getStack(); if (s is LinkedStack) WriteLine("linked");
The is operator can optionally bind a variable. The first example above can be rewritten as
if (abc() is int i) WriteLine(i);
Here is a loop using is
:
while (ReadLine() is string s) WriteLine(s);
In this loop, when ReadLine()
returns a non-null value,
the string variable s receives that value and the loop continues.
When ReadLine()
returns null, the loop terminates.
The as
operator checks whether a value belongs to a
type. If so, it returns the value; otherwise it returns null:
Stack s = getStack(); LinkedStack ls = s as LinkedStack; // if s was not a LinkedStack, ls will be null
A class may inherit from another class. For example:
class LinkedStack : Stack { Node head; public virtual void push(int i) { ... } public virtual int pop() { ... } public bool isEmpty { ... } } class AvgStack : LinkedStack { int count; int sum; public override void push(int i) { base.push(i); count += 1; sum += i; } public override int pop() { int i = base.pop(); count -= 1; sum -= i; return i; } public double average { get => (double) sum / count; } }
An child class may override methods, properties or indexers in
its base class. Only a member marked as virtual
can be
overridden. Any overriding member must include the override
keyword.
A method in a child class can invoke a base class method using the
base
keyword as in the example above.
Note that an object's behavior is determined entirely by its actual class, not by the type of any containing variable. For example:
LinkedStack s = new AvgStack(); s.push(4); s.push(6); AvgStack t = (AvgStack) s; WriteLine(t.average); // writes 5
In this example, the calls to the push
method invoke the
implementation in AvgStack
, even though the containing
variable has type LinkedStack
.