Skip to main content

02 - OOP (2)

Nullable types

Sometimes value types need special state – value not known.

boolean SwitchIsOn = false;

If state of switch is not observed yet, both true and false values are unsuitable. For such cases, declare variable as nullable. Add ? to the type declaration.

boolean? SwitchIsOn = null;

Example of operations with nullable type (int? x):

  • if ( x.HasValue)
  • if (x != null)
  • int y = x ?? 0;
  • (?? - null-coalescing operator) - If value before ?? is not null, use it. Otherwise take value after ??.
  • Pattern matching with someVariable is type newVariableName.
if (x is int valueOfX) {
Console.WriteLine($"x is {valueOfX}"); }
else
{
Console.WriteLine("x does not have a value");
}

? : ?. ?[ ] ??

  • ? : - conditional operator (ternary conditional operator)
    • var classify = (input > 0) ? "positive" : "negative";
  • ?. and ?[] - null-conditional Operators
    • int? length = customers?.Length;
    • Customer first = customers?[0];
    • int? count = customers?[0]?.Orders?.Count();
  • ?? - null-coalescing operator
    • int count = customers?[0]?.Orders?.Count() ?? 0;

Nullable Reference Types

Classically reference types are nullable (i.e. objects can always be null)
This causes often runtime errors (null reference exception)
Nullable reference types and static code analysis tries to help

Null and non-null values

A reference isn't supposed to be null:

  • The variable must be initialized to a non-null value.
  • The variable can never be assigned the value null.

A reference may be null:

  • The variable may only be dereferenced when the compiler can guarantee that the value isn't null.
  • These variables may be initialized with the default null value and may be assigned the value null in other code.
warning

This is purely compile time feature, during execution everything is still nullable. No additional checks or emitted code.

Setup & Requirements

Starting with C# 8.0

Opt in feature:

  • Mandatory in this course and next (Web Apps with C#)
  • Create file named “Directory.Build.props” in solution root folder
  • Convert warnings to errors
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>CS8600,CS8602,CS8603,CS8613,CS8618,CS8625</WarningsAsErrors>
</PropertyGroup>
</Project>

Declaration

Declaration – add ? to the type of variable.

String? foo = null; // ok
String bar = null; // error

Any variable where ? isn’t appended to type is non-nullable ref type.

Null forgiving operator - !


Foo!.Length;

?: ?. ?[] ?? ??=

  • ?: - conditional operator (ternary conditional operator)
    classify = (input > 0) ? "positive" : "negative";
  • ?. and ?[] - null-conditional Operators
    int? length = customers?.Length;
    Customer first = customers?[0];
    int? count = customers?[0]?.Orders?.Count();
  • ?? - null-coalescing operator
    int count = customers?[0]?.Orders?.Count() ?? 0;
  • ??= - null-coalescing assignment operator
    Instead of:
    if (variable is null) { variable = expression; }
    variable ??= expression;

Null ref – Late initialization

Solutions:

  • Initialize in constructor
  • Use backing field with checks
  • Initialize property to null! or default!
public class Person
{
public Person()
{
Name2 = "foo";
}

public string Name1 { get; set; } // Error, not initialized
public string Name2 { get; set; } // Ok, initialized in ctor
private string? _name3;
public string Name3
{
get => _name3 ?? throw new NullReferenceException();
set => _name3 = value;
}
public string Name4 { get; set; } = default!; // or = null!}
}

Null ref - descriptive options

More descriptive options can be added via attributes.

AllowNull - A non-nullable input argument may be null.
DisallowNull - A nullable input argument should never be null.
MaybeNull - A non-nullable return value may be null.
NotNull - A nullable return value will never be null.
MaybeNullWhen - A non-nullable input argument may be null when the method returns the specified bool value.
NotNullWhen - A nullable input argument will not be null when the method returns the specified bool value.
NotNullIfNotNull - A return value isn't null if the argument for the specified parameter isn't null.
DoesNotReturn - A method never returns. In other words, it always throws an exception.
DoesNotReturnIf - This method never returns if the associated bool parameter has the specified value.

Microsoft documentation

Null ref - attributes

ScreenName – allow to set value to null, but get never returns null
ReviewComment – can be null but cannot be set to null

Post-conditions

  • Since T? is not allowed specify possible null return value
  • You can pass in null value, but return value is not null
public class Person
{
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate) { }

public void EnsureCapacity<T>([NotNull] ref T[]? storage, int size) { }
}

Struct

Like a class, but struct is value type. Class is reference type.

Important qualities:

  • Can’t be inherited or used as a base class.
  • Can implement an interface(s)
  • Can’t have default constructor
  • Fields cannot be initialized unless they are declared as const or static.
  • Can be instantiated without the new keyword
  • Fields will remain unassigned and the object cannot be used until all of the fields are initialized. This includes the inability to get or set values through auto-implemented properties.
public struct CoOrds{
public int x, y;
public CoOrds(int p1, int p2)
{
x = p1;
y = p2;
}
}

static void Main()
{
// Initialize:
CoOrds coords1 = new CoOrds();
CoOrds coords2 = new CoOrds(10, 10);
// Declare an object:
CoOrds coords3;
// Initialize:
coords3.x = 10;
coords3.y = 20;
}

When to use Struct?

CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects. AVOID defining a struct unless the type has all of the following characteristics:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

Record C# 9.0

Reference type that provides functionality to capsulate data.
While records can be mutable, they are primarily intended for supporting immutable data models.

  • Concise syntax for creating a reference type with immutable properties
  • Built-in behavior useful for a data-centric reference type:
    • Value equality
    • Concise syntax for nondestructive mutation
    • Built-in formatting for display
  • Support for inheritance hierarchies (from record to record)

Immutable record

public record Person(string FirstName, string LastName);
public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
};

Mutable record

public record Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
};

Records are distinct from classes in that record types use value-based equality. Two variables of a record type are equal if the record type definitions are identical, and if for every field, the values in both records are equal.

C# 10 added record struct to define records as value types. Default is record class.

Boxing

Boxing is the process of converting a value type to the type object or to any interface type implemented by this value type.

Boxing is implicit; unboxing is explicit.

warning

Boxing is expensive!

int i = 123;
// The following line boxes i.
object o = i;

o = 123;
i = (int)o; // unboxing

Tuples

Lightweight, unnamed value types that contain multiple public fields.

var letters = ("a", "b");
Console.WriteLine(letters.Item1);
var alphabetStart = (Alpha: "a", Beta: "b");
(string First, string Second) firstLetters = (Alpha: "a", Beta: "b");
warning

For the last example, left side wins!

Tuples are most useful as return types for private and internal methods. Tuples provide a simple syntax for those methods to return multiple discrete values: You save the work of authoring a class or a struct that defines the type returned. There is no need for creating a new type.

Deconstructing the tuple:

(int max, int min) = Range(numbers); // range returns tuple

Discarding tuple elements:

Discard is a write-only variable whose name is _ (the underscore character). You can assign all of the values that you intend to discard to the single variable.

var (_, _, pop1, _, pop2) = QueryCityDataForYears("New York", 1960, 2010);

Delegates

A delegate is a type that defines a method signature, and can provide a reference to any method with a compatible signature.

You can invoke (or call) the method through the delegate.

Delegates are used to pass methods as arguments to other methods.

public delegate void SampleDelegate(string str);
class SampleClassDelegate
{
// Method that matches the SampleDelegate signature.
public static void SampleMethod(string message)
{
// Add code here.
}
// Method that instantiates the delegate.
void SampleDelegate()
{
SampleDelegate sd = SampleMethod;
sd("Sample string");
}
}

Func<T1,..T16, OutT>, Action<T1,..T16> and Predicate<T> delegates

Three predefined universal (generic) delegates. Action returns void, Predicate returns bool and Func returns user defined type. Number of input paramaters: 0 to 16.

public delegate TResult Func<out TResult>();
public delegate TResult Func<in T,out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
Func<string, string> superFunc1 = (str) => str.ToUpper();
Func<int, int, string> superFunc2 = (x,y) => (x+y).ToString().ToUpper();
var someStr1 = superFunc1("FooBar"); // FOOBAR
var someStr2 = superFunc2(1, 2); // 3
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
Action<string> outputFunc = (str) => Console.WriteLine(str);
outputFunc("Fido");

Parameter type T – can use up to 16 parameters.

public delegate bool Predicate<in T>(T obj);