Have you ever wondered why changes inside a function sometimes affect your variables, and other times do not? offers two ways to pass data: by value (copy) or reference (pointer). This article breaks down the distinction between the two, and how each works. Let's learn to control our data flow and write rock-solid code!
Understanding Value Types and Reference Types
The two main categories Of types are reference and value A value type variable holds an instance Of the type. A reference variable, on the other hand, contains a reference to an instance of the type. Value types are allocated on the stack and deallocated when the stack unwinds or their containing type gets deallocated. This contrasts reference types, which are allocated on the heap and garbage-collected. Value-type variables can be used as fields in reference-type objects. To dive into how .NET treats value and reference types check out Choosing Between Class and Struct
Value types are the built-in types in C# like int
, long
, double
, etc., or user-defined types like struct
types. on the other hand, reference types are like arrays, classes, and so on.
Let's define some value types variables:
int discountRate = 50;
double productAPrice = 60;
Now let's define some reference type variables:
First, let's define our Product class to hold our ProductId, and Price.
public class Product
{
public int ProductId { get; set; }
public int Price { get; set; }
}
Then let's use our class to create a new object from it like:
var product = new Product
{
ProductId = 1,
Price = 60
};
Passing by Value
By default, when we pass a parameter into our method we pass it by value, which means it copies the object value to our method. Therefore, any modifications in our method do not affect the original variable. This applies only to the built-in or struct types.
Let's update the price from our UpdatePriceByPercent
method:
UpdatePriceByPercent(productAPrice, discountRate);
public static void UpdatePriceByPercent(double price, int discountRate)
{
price = price - (price • discountRate) / 100;
}
After calling the method, nothing changed; the value productAPrice
is still the same. Every parameter passed to this method has a copy of the original variable value.
So, we use another approach by defining the CalculateDiscount
method:
public double productANewPrice = CalculateDiscount(ProductAPrice, discountRate);
public static double CalculateDiscount(double price, int discountRate) {
return price - (price * discountRate) / 100;
}
In this approach, we defined a method that returns the new price after applying the discount, assigning this value to a new variable to use later in our program.
Here we passed the price and discount rate as individual parameters and got a new value to deal with. But what if we need to modify the original variable itself? To handle this scenario, we pass the price by reference.
Passing by Reference
In general, passing by reference means you pass a reference that refers to an object. Therefore, we use the variable directly without creating any new copy. This technique is useful in many scenarios. This approach is straightforward in C#. The ref
keyword allows us to access the data and make changes that persist.
ref Keyword
Let's clear things out/ All objects we create from classes are reference-type variables that passed by reference by default, and all objects we create from a struct
are value-type variables that passed by value by default.
Going back to our code, let's update the price within our UpdatePriceByPercentByRef
method:
UpdatePriceByPercentByRef(ref productAPrice, discountRate);
public static void UpdatePriceByPercentByRef(ref double price, int discountRate) {
price = price - (price * discountRate) / 100;
}
We defined UpdatePriceByPercentByRef
method with two parameters. The first double
prefixed with the ref
keyword allows us to use it by reference. On the other hand, the second parameter, an int
, is passed by value. When we call the method, we pass the productAPrice
variable prefixed with the ref
keyword (this is required or throws a compile error). After calling this method, the value of productAPrice
is updated.
out Keyword
Here, we encounter an error if productAPrice
is not initialized before passing it as a ref
parameter. This is true because we are trying to use a reference to an uninitialized variable. But, we can use the out
keyword to use the variable with no need to initialize it. Let's see how to do that:
double price;
MakeItZero(out price);
public static void MakeItZero(out double price) {
price = 0;
}
Here, we assigned the unassigned variable price
to zero.
Arrays
Now, let's use an array of double prices to increase them by 10 percent each:
double[] prices = { 30, 15, 21.5, 50 };
UpdatePrices(prices, 10);
public static void UpdatePrices(double[] prices, int rate) {
for (int i = 0; i < prices.Length; i++)
{
prices[i] = prices[i] + prices[i] * rate / 100;
}
}
We defined UpdatePrices
method that takes an array of double
and an int
. It updates the values in the array by increasing or decreasing them based on the given rate. After this, we define prices as double[]
and initialize with values. Then, we call the UpdatePrices
method and pass the prices array to it. Note that the ref
keyword is not required. Finally, all the values in the array increased by 10%.
Classes and Objects
Let's create a method that takes a product and an int discount rate to update its price:
public static void ApplyDiscount(Product product, int discountRate)
{
product.Price -= (product.Price * discountRate) / 100;
}
Here, there is no need to use the ref
keyword and the price is updated after calling the method.
ref readonly modifier
Then we have ref readonly
which allows referencing a variable without permitting its value to be changed. The question arises: why use it?
We use it to indicate that a large struct
should be passed by reference for reasons.
The ref readonly
modifier introduced in C# 12, offers a balance between and safety.
Key Differences and Considerations
Passing by value is the default behavior for value-type and struct-type variables, and passing by reference is the default behavior for class-type variables or arrays.
We pass by reference when we need to modify the actual value or state, or when we want to gain more performance dealing with large struct-type variables. Being able to use the ref readonly
modifier also avoids unnecessary copying and accesses the heap memory address more safely without allowing modifications.
Conclusion
Passing by value means any modifications in the method body do not affect the original variable value.
Passing by reference means passing the original data location in memory. That means any modifications within the method affect the original variable value.
Built-in types and struct types in C# are passed by value by default. Passing by reference is the default behavior for arrays and classes.
When passing an array or an object to a method, the method gets a reference to the entire object in the memory
We use the ref
keyword to pass variables by reference, and the out
keyword to define a method that returns values by reference.
Consider data size and the need for modification. We use passing by value for small data and passing by reference for large objects to improve performance and control the changes.
Finally, passing by reference can introduce complexity. Use it judiciously, and always with proper data handling practices.
Top comments (1)
Thank you for the great explaination 🔥