Class, Record, and Struct serve as blueprints for creating new objects.
Classes
Classes are the foundational building blocks of Object-Oriented Programming (OOP).
Blueprint and instance
// blueprint
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
}
// instance
var p = new Person { ID = 1, Name = "Mirza" }
Inspection
Printing the class instance in the console will just display it's name:
var p1 = new Person { ID = 1, Name = "Mirza" };
Console.WriteLine(p1); // Person
To print the actual, we'd need print each property explicitly:
Console.WriteLine(p1.ID); // 1
Console.WriteLine(p1.Name); // "Mirza"
Mutation
C# classes are mutable, meaning the originally set values can be altered:
var p1 = new Person { ID = 1, Name = "Mirza" };
Console.WriteLine(p1.Name); // "Mirza"
p1.Name = "Armin";
Console.WriteLine(p1.Name); // "Armin"
That said, this can be tweaked by changing the accessor the class property.
public class Person
{
public int ID { get; set; }
public string Name { get; init; } // <--
}
This time around if we try to change the value of the Name property after initialization, the C# compiler will start to complain.
var p1 = new Person { ID = 1, Name = "Mirza" };
p1.Name = "Edis"; // β
The init accessor allows you to create properties that can only be assigned a value during object initialization.
Memory location
Classes are reference types and are allocated on the heap. If we create two distinct class objects and assign one to the other, both objects will point to the exact same reference in memory:
var p1 = new Person { ID = 1, Name = "Mirza" };
var p2 = new Person { ID = 2, Name = "Mirza" };
p1 = p2;
p1.Name = "Sead";
Console.WriteLine(p1.Name); // Sead
Console.WriteLine(p2.Name); // Sead
If we change a value in one of the objects, it will be reflected in both.
Equality
Two class instances (objects) aren't equal even if they share the same values:
var p1 = new Person { ID = 1, Name = "Mirza" };
var p2 = new Person { ID = 1, Name = "Mirza" };
Console.WriteLine(p1 == p2); // False
Console.WriteLine(p1.Equals(p2)); // False
Console.WriteLine(ReferenceEquals(p1, p2)); // False
To compare the two, we'd need to check each property on each class and compare to the other:
private static bool AreEqual(Person? x, Person? y)
{
// If both are the same memory instance (or both null), they are equal
if (ReferenceEquals(x, y)) return true;
// If only one is null, they are not equal
if (x is null || y is null) return false;
// If actual values are equal
return x.ID == y.ID && x.Name == y.Name;
}
In this case, the values of these two instances would indeed be equal.
Console.WriteLine(AreEqual(p1, p2)); // True
Deconstruction
Classes support deconstruction through manual implementation of the Deconstruct method:
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
public void Deconstruct(out int id, out string name)
{
id = ID;
name = Name;
}
}
Usage:
var p = new Person { ID = 1, Name = "Mirza" };
var (id, name) = p;
Console.WriteLine(id); // 1
Console.WriteLine(name); // "Mirza"
Structs
Structs are lightweight data structures designed for small, simple data values.
Blueprint and instance
// blueprint
public struct Person
{
public int ID { get; set; }
public string Name { get; set; }
}
// instance
var p = new Person { ID = 1, Name = "Mirza" };
Inspection
Just like classes, structs cannot print all contents at once:
Console.WriteLine(p); // Person
Console.WriteLine(p.ID); // 1
Console.WriteLine(p.Name); // Mirza
Mutation
Structs also allow mutation.
var p = new Person { ID = 1, Name = "Mirza" };
Console.WriteLine(p.Name); // Mirza
p.Name = "Armin";
Console.WriteLine(p.Name); // Armin
Unless we forbid it using the init accessor.
public struct Person
{
public int ID { get; set; }
public string Name { get; init; } // <--
}
Memory location
The structs are value types and are (usually) allocated on the stack.
var p1 = new Person { ID = 1, Name = "Mirza" };
var p2 = new Person { ID = 2, Name = "Mirza" };
p1 = p2;
p1.Name = "Sead";
Console.WriteLine(p1.Name); // Sead
Console.WriteLine(p2.Name); // Mirza
If we change a value in one of the objects, it won't reflect in both of them.
Equality
Structs are compared by value.
var p1 = new Person { ID = 1, Name = "Mirza" };
var p2 = new Person { ID = 1, Name = "Mirza" };
Console.WriteLine(p1.Equals(p2)); // True
Deconstruction
Structs support deconstruction through manual implementation of the Deconstruct method:
public struct Person
{
public int ID { get; set; }
public string Name { get; set; }
public readonly void Deconstruct(out int id, out string name)
{
id = ID;
name = Name;
}
}
Usage:
var p = new Person { ID = 1, Name = "Mirza" };
var (id, name) = p;
Console.WriteLine(id); // 1
Console.WriteLine(name); // "Mirza"
Records
Records are a newer type optimized for data-centric modeling, immutability, and built-in value equality. It brings the best of classes and structs.
Blueprint and instance
// blueprint
public record Person (int ID, string Name);
// instance
var p = new Person(1, "Mirza");
The shorter syntax is the equivalent of doing:
// blueprint
public record Person
{
public int ID { get; init; }
public string Name { get; init; }
}
// instance
var p = new Person { ID = 1, Name = "Mirza" };
Inspection
Printing the record instance will print the record and its contents:
var p = new Person(1, "Mirza");
Console.WriteLine(p); // Person { ID = 1, Name = Mirza }
Mutation
Records are immutable by default:
var p = new Person(1, "Mirza");
p.Name = "Edis"; // β
Console.WriteLine(p);
Unless the record is declared using the class-like syntax and we change the default accessor from init to set:
public record Person
{
public int ID { get; set; } // <-- defaults to init
public string Name { get; set; } // <-- defaults to init
}
var p = new Person { ID = 1, Name = "Mirza" };
p.Name = "Edis"; // βοΈ
Console.WriteLine(p.Name); // Edis
Memory location
The records are reference types and are allocated on the heap.
var p1 = new Person { ID = 1, Name = "Mirza" };
var p2 = new Person { ID = 1, Name = "Mirza" };
p1 = p2;
p1.Name = "Sead";
Console.WriteLine(p1.Name); // Sead
Console.WriteLine(p2.Name); // Sead
If we change a value in one of the objects, it will be reflected in both.
Equality
Just like Structs, Records are compared by value.
var p1 = new Person(1, "Mirza");
var p2 = new Person(1, "Mirza");
Console.WriteLine(p1 == p2); // True
Console.WriteLine(p1.Equals(p2)); // True
Console.WriteLine(Object.ReferenceEquals(p1, p2)); // False
Two objects share the same values, but do not point to the same reference in memory.
Deconstruction
Records support deconstruction out of the box.
var p = new Person(1, "Mirza");
var (id, name) = p;
Console.WriteLine(id); // 1
Console.WriteLine(name); // "Mirza"
Bonus: Record Structs
Aside from Class-based Records, we can also create Records that behave as Structs.
- Value type equality
- Allocated (usually) on the stack
- Mutable by default
- All other benefits of Records
// blueprint
public record struct Person(int ID, string Name);
Until next time π
Top comments (0)