Skip to main content
  1. Application Level/

Meaningful Types

A meaningful review of managed programming languages value and reference type semantics

Value Type Semantics #

Value type variables differ from reference type variables in the following contexts.

Assignment #

The assignment operator “=” is often the copy operator. For value types, the assignment operator copies the value from one storage location to another.

// C#
int valVarX = 3; 
int* ptrToValVarX = &valVarX;
Console.WriteLine($"valVarX address = {(long)ptrToValVarX}");
// Output: valVarX address = 140732702082740

// Copy (Assign) the content of valVarX to valVarY
int valVarY = valVarX; 

int* ptrToValVarY = &valVarY;
Console.WriteLine($"valVarY address = {(long)ptrToValVarY}");
// Output: valVarY address = 140732702082724
// valVarY has a different address than valVarX

Console.WriteLine(valVarY); 
// Output: 3 the value that was copied from valVarX

valVarY = 10; 
Console.WriteLine ($"{valVarY} and {valVarX}")
// Output: 10 and 3
// The update to valVaryY does not mutate valVarX

Equality and Comparison #

For value types, equality and comparison operations test the values in the respective storage locations.

// JavaScript
let valVarX = 3; 
let valVarY = 10; 

console.log(valVarX === valVarY); // Output: false
console.log(valVarX < valVarY); // Output: true
// C#
// A struct is a value type 
public struct Foo
{
    public int x;
    public int y; 
    public Foo (int x, int y) { this.x = x; this.y = y; }    
}

var f1 = new Foo(1, 1); 
var f2 = new Foo(1, 1);
Console.WriteLine(f1.Equals(f2));
// Output: True 
// C# struct's default to structural equality
// Each struct member is tested for equality 
// N.B. The Value Type class overrode Object's virtual Equals method
// N.B. == and relational operators are not implemented for structs 

Pass by Value (Pass by Copy) #

Pass by Value (Copy) is a method of passing arguments to a function. In C#, it is one of two options. In JavaScript, it is the only option.

Passing a variable as an argument to a function by value allocates a new variable inside the called function that receives a copy of the argument’s content. The function may mutate the copy variable but that does not change the source variable.

// C#
int x = 256; 
int* ptrToNbr = &x;
Console.WriteLine($"address of x {(long)ptrToNbr}"); 
// address of x 140732774508176

TestPassIntByValue(x); // pass x by value 
Console.WriteLine($"x's value {x}"); // 256 

static unsafe void TestPassIntByValue(int copy)
{
    int* ptrToLocalIntCopy = &copy;
    Console.WriteLine($"address of copy {(long)ptrToLocalIntCopy}");
    
    // address of copy is 140732774507260 
    // copy is not the same storage location as x

    copy = 800; // mutate the copy variable
}

Passing by Value (Copy) of value types helps minimize side effects in a code base. For performance critical scenarios on a hot code path with large value types, it might make sense to pass by reference to reduce the memory allocations associated with copying.

Pass by Reference (Pass by Alias) #

In C#, passing a variable as an argument to a function by reference (alias) does not create an allocation. It does not create a new variable or storage location inside the called function.

The called function receives an alias to the argument. A new name. That’s it. The alias is the same storage location as the argument. The alias and the variable are the same instance.

// C#
int x = 256;
Console.WriteLine($"Address of x {(long)ptrToNbr}"); 
// Address of x 140732664694432

TestPassIntByRef(ref x); // pass x by reference 
Console.WriteLine($"Value of x after passing by reference {x}"); 
// Output: 300

static unsafe void TestPassIntByRef(ref int alias)
{        
    fixed (int* ptrToAlias = &alias)
    {
        Console.WriteLine($"alias address {(long)ptrToAlias}"); 
        // The address of alias is 140732664694432
        // alias is the same instance as x
    }
        
    alias = 300; // this mutates x as it is the same instance 
}

The “In” Parameter Modifier (Deep Immutability) #

Below is an example of passing a C# struct by reference with the “in” modifier. The “in” modifier passes the argument by reference but prevents the function from modifying the argument. It eliminates the pass by value overhead of the value type copy operation, but retains the pass by value feature of preventing the function from mutating the argument.

// C# 
public struct Point
{
    public int x;
    public int y;
    public Point (int x, int y) { this.x = x; this.y = y; }
    public override string ToString()
    {
        return $"x: {x} y: {y}";
    } 
}

var p1 = new Point(1,1); 
p1.x = 5;
Console.WriteLine(p1.ToString());
// Output: "x: 5 y: 1

Point* ptrToStructP1 = &p1;
Console.WriteLine($"address of p1 {(long)ptrToStructP1}");
// Output: address of p1 140732811376440

PassStructByRef(p1); // pass by reference 

 static unsafe void PassStructByRef(in Point alias)
{
       
    fixed (Point* ptrToStructAlias = &alias)
    {
        Console.WriteLine($"address of alias {(long)ptrToStructAlias}");
    }
    // Output: address of alias 140732811376440
    // p1 and alias are the same instance 
    
    alias.x = 10; 
    // runtime fatal error 
    // "error CS8332: Cannot assign to a member
    // of variable 'in Program.Point' or use 
    // it as the right hand side of a ref assignment 
    // because it is a readonly variable"
    // "in" modifier prevents mutation of the argument
}

The in modifier works very well for scenarios

  • with large structs
  • a need to avoid the expense of copying the struct on a pass by value call
  • and ensure the called function does not mutate the struct

N.B. C# also has the “out” parameter modifier. It is like the ref modifier, except it does not require the assignment of a value to the argument before going into the function. It is commonly used to get multiple return values back from a function.

Structs and Collections #

// C# 
public struct Point
{
    public int x;
    public int y;
    public Point (int x, int y) { this.x = x; this.y = y; }
    public override string ToString()
    {
        return $"x: {x} y: {y}";
    } 
}

// declarations 
var myPointsArray = new Point[5]; 
var myPointsList = new List<Point>(5); 
var myPoint = new Point(3, 7);

// copy myPoint into the 1st element of the array
myPointsArray[0] = myPoint; 

// copy myPoint into the 1st element of the list 
myPointsList.Add(myPoint);

myPointsArray[0].x = 5;  // change x 
Console.WriteLine(myPointsArray[0]);
// Output: x: 5 y: 7
// the array indexer returns a reference 
// to the struct. 

myPointsList[0].x = 6; 
// "error CS1612: Cannot modify the 
// return value of 'List<Program.Point>.this[int]' 
// because it is not a variable"

// Unlike an array, myPointsList[0].x is a call to 
// the List class indexer "get" method behind the scenes.
// This is a pass by value method call so the List 
// indexer is returning a copy of the struct.
// We are attempting to set the x property on a copy
// but we are not storing the copy in a variable.
// Hence the error message 

// The .Net List<> class indexer method source below 
// Sets or Gets the element at the given index.
public T this[int index]
{
    get
    {
        if ((uint)index >= (uint)_size)
        {
            ThrowHelper.ThrowArgumentOutOfRange_IndexMustBeLessException();
        }
        return _items[index];
    }

    set
    {
        // the setter code is not important for our example
    }
}
// C# struct and List<> example continued 

// this works 
var temp = myPointsList[0]; 
temp.x = 6;
myPointsList[0] = temp;  
Console.WriteLine(myPointsList[0]);
// Output: x: 6 y: 7 

// so does this 
myPointsList[0] = myPointsList[0] with {y = 20};
Console.WriteLine(myPointsList[0]);
// Output: x: 6 y: 20 

Reference Semantics #

How reference type variables differ from value type variables in the assignment, equality, and argument passing contexts.

Assignment #

The assignment operator “=” copies the reference (address) from one storage location to another.

// C#
char[] characters = {'A', 'B', 'C'};
fixed (char* ptrToCharacters = characters)
{
    Console.WriteLine($"characters -> array {(long)ptrToCharacters}");
    // Output: 6690857328
    // The address of the first element in the characters array 
    // The characters variable's content is an address pointing to 'A'

    Console.WriteLine($"Dereferencing ptrToCharacters {*ptrToCharacters}");
    // Output: A

    Console.WriteLine(characters[0]);
    // Output: A
}

char[] letters = characters; 
// copies the reference (the address of 'A') to the letters variable 
  
fixed (char* ptrToLetters = letters)
{
    Console.WriteLine($"letters -> array {(long)ptrToLetters}");
    // Output: 6690857328
    // The address of the first element in the array 
    // The letters variable's content is an address pointing to 'A'

    Console.WriteLine($"Dereferencing ptrToLetters {*ptrToLetters}");
    // Output: A 

    Console.WriteLine(letters[0]);
    // Output: A 
}

letters[0] = 'Z'; // mutates the characters array 

// looping through the characters array 
foreach (char character in characters)
{
    Console.WriteLine($"{character}");
    // Output: Z, B, C 
}

letters = new char[] {'M', 'N', 'O'};
    
fixed (char* ptrToLetters = letters)
{
    Console.WriteLine($"letters -> array {(long)ptrToLetters}");
    // Output: 6690857768
    // letters no longer points to same location as characters 
}

foreach (char character in characters)
{
    Console.WriteLine($"{character}");
    Output: Z, B, C 
}

Equality #

For reference types, equality operations test the addresses in the respective storage locations.

// C#
public class Bar 
{
    private int x; 
    private int y; 
    public Bar (int x, int y) {this.x = x; this.y = y;}
}

var b1 = new Bar(1, 1);
var b2 = new Bar(1,1); 

Console.WriteLine(b1.Equals(b2));
// Output: False 
// b1 stores an address that does not equal the address b2 stores 

Console.WriteLine(b1 == b2);
// Output: False 

var b3 = b1; 

Console.WriteLine(b1.Equals(b3));
// Output: True 
// b1 stores and address that is equal to the address that b3 stores 

Console.WriteLine(b1 == b3);
// Output: True 

// N.B. Relational operators are not implemented for reference types 
// C#
char[] characters = {'A', 'B', 'C'};

// "=" copies characters' content, an address, to ptrToCharacters
fixed (char* ptrToCharacters = characters)
{
    Console.WriteLine($"characters -> array {(long)ptrToCharacters}");
    // Output: 6690857328
    // the characters variable stores this address 
}

// copies the reference (the address of 'A') to the letters variable 
char[] letters = characters; 

// "=" copies letters content, an address, to ptrToLetters
fixed (char* ptrToLetters = letters)
{
    Console.WriteLine($"letters -> array {(long)ptrToLetters}");
    // Output: 6690857328
    // The letters variable stores this address 
   
}   

Console.WriteLine($"letters == characters? {letters == characters}");
// Output: True 
// letters and characters both store 6690857328 so they are equal 

Pass by Value (Pass by Copy) #

When passing a reference type by value, a copy of the address stored by the reference type is passed to a new reference type variable in the function. Because the function has a copy of the address, it can change the referenced value or object.

// C#
int[] numbers = {10,20,30,50};
   
fixed (int* ptrToArray = numbers)
{
    Console.WriteLine($"numbers[0] address {(long)ptrToArray}");
    //  Output: 6709765296
    //  The address of "10" pointed to by numbers
}

TestPassByValueArray(numbers); // pass by value
Console.WriteLine(${numbers[0]});
// Output: 1
// The function changed 10 to 1 
// numbers is not null because copy is a separate variable 

static unsafe void TestPassByValueArray(int[] copy)
{

// copy is a new storage allocation 
fixed (int* ptrToArray = copy)
    {
        Console.WriteLine ($"copy[0] address {(long)ptrToArray}");
        // Output: 6709765296
        // copy also points to "10"
    }
    
    copy[0] = 1; // changes 10 to 1 
    copy = null; 
}

Pass by Reference (Pass by Alias) #

The argument is passed to the function with a new name. There is no copy operation. There is no new storage allocation. The argument and the parameter are the same variable instance.

// C#
int[] numbers = {10,20,30,50};
   
fixed (int* ptrToArray = numbers)
{
    Console.WriteLine($"numbers[0] {(long)ptrToArray}");
    //  Output: 6709765296
    //  The address of "10" pointed to by numbers
}

TestPassByRefArray(ref numbers); // pass by reference 
Console.WriteLine($"Is numbers array null? {numbers == null}");
// True 

// alias is numbers with a new name. 
static unsafe void TestPassByRefArray(ref int[] alias)
{
    fixed (int* ptrToArray = alias)
    {
        Console.WriteLine($"array[0] address {(long)ptrToArray}");
        // Output: 6709765296
        // alias also points to "10"
    }

    alias[0] = 3; // changes the 10 to a 3 
    alias = null; 
}

The “In” Parameter Modifier (Shallow Immutability) #

// C#
int[] numbers = {10,20,30,50};

PassByRefWithIn(numbers); // pass by reference see "in" below
Console.WriteLine($"numbers[0] after pass by ref with in {numbers[0]}");
// Output: 50 

PassByRefWithInMutateArgument(numbers); // pass by reference see "in" below


// The "in" parameter modifier prevents mutation of the argument
// When the argument is a reference type
// and the referenced object is mutable 
// the referenced object can be mutated 
// Shallow immutability not deep immutability 
static void PassByRefWithIn (in int[] alias)
{
    alias[0] = 50;
}

// The "in" parameter prevents mutation of the argument 
static void PassByRefWithInMutateArgument (in int[] alias)
{
    alias = null;  // compile time error  
}

The Special Case of Strings #

Strings are a reference type. String’s equality operators, however, have been overridden to follow value type semantics.

// C#
string stringTest = "original";

// copy stringTest's content, an address, to ptrToStringTest
fixed (char* ptrToStringTest = stringTest)
{
    Console.WriteLine($"stringTest[0] address {(long)ptrToStringTest}");
    // Output: 6564526228
}

// copy stringTest's content, an address, to assignedString
string assignedString = stringTest; 

// copy assignedString's content, an address, to ptrToAssignedString
fixed (char* ptrToAssignedString = assignedString)
{
    Console.WriteLine($"assignedString[0] address {(long)ptrToAssignedString}");
    // Output: 6564526228
    // assignedString also points to the same location
    // String is a reference type 
}

string newString = "original";
fixed (char* ptrToNewString = newString)
{
    Console.WriteLine($"newString[0] address {(long)ptrToNewString}");
    // Output: 6617323668
    // Not the same address as stringTest
}

Console.WriteLine(newString == stringTest);
// Output: True (Even though they have different addresses)
// Value type equality semantics. 

C# strings are also immutable.

// C#  
assignedString = "assigned"; // change from "original" to "assigned" 

// copy assigned String's reference to ptrToAssignedString 
fixed (char* ptrToAssignedString = assignedString)
{
    Console.WriteLine($"assignedString[0] address {(long)ptrToAssignedString}");
    // Output: 6617323860 
    // Assigning a new value to assignedString 
    // does not mutate "original" at 6564526228
    // It creates a new string and points 
    // assignedString at this new address 
}

Console.WriteLine ($"stringTest value {stringTest}");
// Output: "original" 
// There was no destructive read in at 6564526228