C# & .NET (C# 14, .NET 10 LTS) Module 2 – Types, variables and operators
Value types and reference types
What C# copies when you assign or pass a struct, a class, an array or a string, why one change can show through two variables, and where the stack misleads.
What you will learn
- Predict what assignment and argument passing copy for structs, classes, arrays and strings
- Detect accidental sharing of a mutable object, and remove it with a deep copy
- Explain why the kind of a type, not the stack or the heap, decides how its values are copied
- Compare structs, classes, records and strings by equality, default value and null
Before you start
What you’ll learn · Before you start
What you will learn
- Predict what assignment and argument passing copy for structs, classes, arrays and strings
- Detect accidental sharing of a mutable object, and remove it with a deep copy
- Explain why the kind of a type, not the stack or the heap, decides how its values are copied
- Compare structs, classes, records and strings by equality, default value and null
Before you start
On this page
b = a copies a Point struct, so setting b.X to 9 leaves a.X at 1. Declare Point as a class instead, and the
same two lines change a.X to 9 as well, because a and b now name one object. Every C# type is one of two kinds,
value type: A type whose variables hold the value itself, such as int, a struct or an enum. Assigning or passing such a value copies it, so a change to the copy never reaches the original. and reference type: A type whose variables hold a reference to an object, such as a class, string or array. Assigning or passing such a variable copies the reference, so both variables lead to the same object., and the kind decides what an
assignment, an argument or a list entry copies.
You will predict what each assignment and call copies, catch an object shared by accident, and tell value types from reference types by what they copy, not by where they live.
Assigning a struct copies its data; assigning a class copies a reference
A variable of a value type holds its data, and assignment copies that data. A variable of a reference type holds a reference to an object, and assignment copies the reference, so both variables lead to the same object. This program runs the same three lines on a struct and on a class:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// The same two fields, once in a struct and once in a class.
PointStruct a = new() { X = 1, Y = 2 };
PointStruct b = a;
b.X = 9;
Console.WriteLine($"struct: a = {a}, b = {b}");
PointClass c = new() { X = 1, Y = 2 };
PointClass d = c;
d.X = 9;
Console.WriteLine($"class: c = {c}, d = {d}");
Console.WriteLine($"c and d are one object: {ReferenceEquals(c, d)}");
struct PointStruct
{
public int X;
public int Y;
public override readonly string ToString() => $"({X}, {Y})";
}
class PointClass
{
public int X;
public int Y;
public override string ToString() => $"({X}, {Y})";
} Output
struct: a = (1, 2), b = (9, 2) class: c = (9, 2), d = (9, 2) c and d are one object: True
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run copy.cs
b = a copied both fields of the struct, so setting b.X to 9 changed only b. d = c copied a reference, so
d.X = 9 changed the one object that c names too, and ReferenceEquals(c, d) confirmed that there is only one.
Step through what each line did:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- a holds its data. PointStruct a = new() { X = 1, Y = 2 } puts the numbers 1 and 2 in a itself. A variable of a value type contains its value.
- b = a. b = a copies both fields into b. There are now two points, each with its own X and Y.
- b.X = 9. Setting b.X changes b alone. a still holds X 1, as copy.cs printed: a = (1, 2), b = (9, 2).
- c refers to an object. new PointClass { X = 1, Y = 2 } creates an object of its own, and c holds a reference to it, not the numbers.
- d = c. d = c copies the reference, not the object. d now leads to the same object as c: there is still one point.
- d.X = 9. d.X = 9 changes the object, and c sees the change through its own reference: copy.cs printed c = (9, 2), d = (9, 2).
- One object, two values. ReferenceEquals(c, d) printed True: two variables, one object. The struct gave two separate values from the first assignment on.
Microsoft’s reference states the same rule: a variable of a value type contains an instance of the type, while a variable of a reference type contains a reference to one.
Key idea
Assignment always copies what a variable holds: the data itself for value types, a reference for reference types. That is why two variables of a class type can change one object.
Checkpoint
Check: What assignment copies
A method gets a copy too: of the value, or of the reference
Passing an argument is an assignment to the parameter, so the same rule applies. By default C# passes every argument
by value: the method gets a copy of a struct, or a copy of a reference. This program calls the same Shift code with a
struct and with a class, then replaces a parameter, changes an array and changes a string:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
PointStruct s = new() { X = 1, Y = 2 };
Moves.Shift(s);
Console.WriteLine($"after Shift(struct): s = {s}");
PointClass c = new() { X = 1, Y = 2 };
Moves.Shift(c);
Console.WriteLine($"after Shift(class): c = {c}");
Moves.Replace(c);
Console.WriteLine($"after Replace(class): c = {c}");
int[] marks = [40, 55, 70];
Moves.AddGrace(marks);
Console.WriteLine($"after AddGrace(int[]): marks = {string.Join(", ", marks)}");
string city = "pune";
Moves.Shout(city);
Console.WriteLine($"after Shout(string): city = {city}");
static class Moves
{
// The same code twice: it changes the method's own copy of a struct, or the one object a class reference names.
public static void Shift(PointStruct p) => p.X += 10;
public static void Shift(PointClass p) => p.X += 10;
// Assigning to the parameter changes only the method's own variable.
public static void Replace(PointClass p) => p = new PointClass { X = 100, Y = 100 };
public static void AddGrace(int[] values)
{
for (int i = 0; i < values.Length; i++)
{
values[i] += 5;
}
}
public static void Shout(string text) => text = text.ToUpperInvariant();
}
struct PointStruct
{
public int X;
public int Y;
public override readonly string ToString() => $"({X}, {Y})";
}
class PointClass
{
public int X;
public int Y;
public override string ToString() => $"({X}, {Y})";
} Output
after Shift(struct): s = (1, 2) after Shift(class): c = (11, 2) after Replace(class): c = (11, 2) after AddGrace(int[]): marks = 45, 60, 75 after Shout(string): city = pune
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run passing.cs
Shift(s) changed the method’s own copy, so s stayed (1, 2), while Shift(c) followed its copy of the reference to
the caller’s object, which became (11, 2). Replace(c) gave its parameter a new object, which changed only the
method’s variable. An array is a reference type, so AddGrace changed the caller’s marks, and Shout left city
alone, because ToUpperInvariant returns a new string.
| Passed to a method | The method gets | Does a change show in the caller? |
|---|---|---|
| a struct | a copy of its fields | no |
| an object of a class | a copy of the reference | a change to the object does; a new object does not |
| an array | a copy of the reference | a change to an element does |
| a string | a copy of the reference | no: every change makes a new string |
The ref, out and in modifiers change this rule; the module on methods covers them.
Expecting a new object to reach the caller
p = new PointClass { X = 100 } inside a method replaces the method’s own reference and nothing else. To hand the
caller a new object, return it: c = MakePoint();.
Checkpoint
Check: Arguments passed by value
Which types are value types, and which are reference types
Type.IsValueType says which kind a type is:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// Type.IsValueType says which of the two kinds a type is.
(string Name, Type Type)[] types =
[
("int", typeof(int)),
("double", typeof(double)),
("decimal", typeof(decimal)),
("bool", typeof(bool)),
("char", typeof(char)),
("DateTime", typeof(DateTime)),
("DayOfWeek, an enum", typeof(DayOfWeek)),
("(int, string), a tuple", typeof((int, string))),
("int?", typeof(int?)),
("PointStruct", typeof(PointStruct)),
("string", typeof(string)),
("int[]", typeof(int[])),
("object", typeof(object)),
("List<int>", typeof(List<int>)),
("Action, a delegate", typeof(Action)),
("PointClass", typeof(PointClass)),
("Booking, a record", typeof(Booking)),
("Fare, a record struct", typeof(Fare)),
];
foreach ((string name, Type type) in types)
{
Console.WriteLine($"{name,-24} {(type.IsValueType ? "value type" : "reference type")}");
}
// string is a reference type that behaves like a value: no change shows through another variable.
Console.WriteLine();
string city = "Pune";
string copy = city;
copy += " Junction";
Console.WriteLine($"city = {city}, copy = {copy}");
string built = new(['P', 'u', 'n', 'e']);
Console.WriteLine($"built == city: {built == city}, one object: {ReferenceEquals(built, city)}");
struct PointStruct
{
public int X;
public int Y;
}
class PointClass
{
public int X;
public int Y;
}
record Booking(string Pnr);
record struct Fare(decimal Amount); Output
int value type double value type decimal value type bool value type char value type DateTime value type DayOfWeek, an enum value type (int, string), a tuple value type int? value type PointStruct value type string reference type int[] reference type object reference type List<int> reference type Action, a delegate reference type PointClass reference type Booking, a record reference type Fare, a record struct value type city = Pune, copy = Pune Junction built == city: True, one object: False
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run kinds.cs
The numeric types, bool, char, DateTime, enums, tuples such as (int, string), nullable value types such as int? and
every struct are value types. string, arrays, object, collections such as List<int>, delegates, classes and
records are reference types, while record struct declares a record that is a value type. An array is a reference
type even when its elements are numbers. The kind matters most for the types you did not write: a DateTime passed to
a method is a copy, while a List<int> passed the same way is the caller’s own list.
string is the reference type that behaves like a value. copy += " Junction" made a new string and left city
unchanged, and built == city was true although ReferenceEquals showed two objects, because == on strings
compares the text.
Checkpoint
Check: Which types are value types
Accidental sharing, reproduced and fixed with deep copies Pro: opens with a Pro, Premium or Ultimate pass
Most bugs with reference types come from two parts of a program changing one object while each believes it has its own. The runs below reproduce five ways this happens, then the three statements that C# refuses for structs because they would change or compare the wrong thing.
In this part:
- Five ways to share an object without meaning to
- Three struct statements that C# refuses, and what to write instead
4 runnable examples (C#, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Where values really live: the heap, the stack and the JIT, measured Pro: opens with a Pro, Premium or Ultimate pass
The stack and the heap are the most repeated explanation of value types. The run below counts the heap bytes of ten small pieces of code, and shows what really decides where a value lives.
In this part:
- Ten pieces of code, their heap allocations counted
1 runnable example (C#) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Equality, defaults and null for each kind, compared Pro: opens with a Pro, Premium or Ultimate pass
What == compares, what a variable holds before anything is assigned to it and whether it can be null all follow
from the kind of its type. The run below compares two equal values of each kind, and counts them in a HashSet.
In this part:
- What equality compares for each kind, and how a HashSet counts
- How a record compares its members
- Defaults and null
1 runnable example (C#) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: predict, then run
Change the struct to a class
Copy passing.cs to your computer and turn struct PointStruct into class PointStruct. Before you run it with
dotnet run passing.cs, predict the first line of output; then run it and explain the change in one sentence.
Then answer the questions on copies, arguments and the two kinds of types, which begin where your answers so far are weakest:
Practice that adapts to you
Practise this lesson
9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between a value type and a reference type in C#?
A variable of a value type, such as int, double, a struct or an enum, holds the value itself, so assignment and
argument passing copy the value, and a change to the copy never reaches the original. A variable of a reference type,
such as a class, string, an array or a delegate, holds a reference to an object, so assignment copies the reference
and both variables lead to the same object: a change made through one shows through the other. string is a
reference type that cannot change, so it behaves like a value. Where the memory lives is not the defining difference;
what a copy copies is.
-
Mid to senior
A shop starts a reorder with new Order { Items = old.Items }. Customers report that changing a quantity in the reorder changes their past order too, and copying the list did not fix it. Explain the bug, fix it, and say how you would test for it.
-
Mid to senior
A teammate says that structs live on the stack and classes on the heap, so all the service's data transfer objects should become structs to save memory. What is right and what is wrong in this, and when is a struct the better choice?
-
Fresher to mid
A customer is loaded twice from the database as two objects of a plain Customer class, and both end up in a HashSet<Customer>, although their Id is the same. Why does the set keep both, and what are your options to make it treat them as one customer?
Key takeaways
- A variable of a value type holds its data, and assignment copies the data, so
b = agives two separate structs. - A variable of a reference type holds a reference, and assignment copies the reference, so
d = cgives two names for one object. - Arguments are passed by value: a method gets a copy of a struct or a copy of a reference, so it can change the caller’s object but cannot replace it.
- Numbers,
bool,char, enums, tuples and structs are value types, while classes, records, arrays, delegates andstringare reference types, andstringbehaves like a value because it never changes.
Terms in this lesson
- reference type
- A type whose variables hold a reference to an object, such as a class, string or array. Assigning or passing such a variable copies the reference, so both variables lead to the same object.
- value type
- A type whose variables hold the value itself, such as int, a struct or an enum. Assigning or passing such a value copies it, so a change to the copy never reaches the original.
References
- Value types (Microsoft Learn)
- Reference types (Microsoft Learn)
- Method parameters (passing arguments by value) (Microsoft Learn)
- Type.IsValueType property (Microsoft Learn)
- Compiler Error CS1612 (Microsoft Learn)
- What's new in the .NET 10 runtime (stack allocation and escape analysis) (Microsoft Learn)
- GC.GetAllocatedBytesForCurrentThread method (Microsoft Learn)
- ValueType.Equals method (Microsoft Learn)
- Equality operators (string and reference equality) (Microsoft Learn)
- C# equality comparisons (Microsoft Learn)
- Choosing between class and struct (Framework Design Guidelines) (Microsoft Learn)
- Records (value equality) (Microsoft Learn)
- Nullable reference types (Microsoft Learn)
- The Java Language Specification, Java SE 25: Types, Values, and Variables (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress