Properties and Indexers

( 1 user )

Property

A Property in C# is a clean and controlled way to store and get data in a class. Now, from this statement, the first thing you would be thinking is - We were always doing this with the help of class variables then why to use properties to do the same thing again?

To answer this question, let's take a small example. Consider a class say "SavingsAccount" that has a variable called "amount". It also has a method "DeductAmount(...)" which can deduct the given amount from the account.


class SavingsAccount
{
	public float amount;
	
	public void DeductAmount(float amount)
	{
		if(amount > this.amount)
		{
			Console.WriteLine("Cannot deduct the amount. Balance is not sufficient");
		}
		else
		{
			this.amount -= amount;
			Console.WriteLine("Amount deducted successfully");
		}
	}
}
		

Now, in our main program, we can simply set and get the data variable "amount".


class Program
{
	static void Main(string[] args)
	{
		var aliceAccount = new SavingsAccount();
		aliceAccount.amount = 1000.00f;
		
		Console.WriteLine($"Alice's saving's account has total amount of {aliceAccount.amount}");
		
		aliceAccount.DeductAmount(1500.00f);
		
		Console.WriteLine($"Balance is now {aliceAccount.amount}");
	}
}
		

This will output "Cannot deduct the amount. Balance is not sufficient". This is fine but the problem here is the variable "amount" is exposed in our main method. So instead of calling the "DeductAmount(..)" method, someone could directly deduct the amount from Alice's account without checking if the balance is sufficient or not. For e.g.


class Program
{
	static void Main(string[] args)
	{
		var aliceAccount = new SavingsAccount();
		aliceAccount.amount = 1000.00f;
		
		Console.WriteLine($"Alice's saving's account has total amount of {aliceAccount.amount}");
		
		aliceAccount.amount -= 1500.00f;
		
		Console.WriteLine($"Balance is now {aliceAccount.amount}");
	}
}
		

The above program will output "Balance is now -500.00f". So we need to make this "amount" variable as private and expose methods to have more validations over our data variable "amount". Let's modify the class.


class SavingsAccount
{
	float amount;
	
	public void SetAmount(float amount)
	{
		if(amount > 0)
		{
			this.amount = amount;
		}
	}
	
	public float GetAmount()
	{
		return this.amount;
	}
	
	public void DeductAmount(float amount)
	{
		if(amount > this.amount)
		{
			Console.WriteLine("Cannot deduct the amount. Balance is not sufficient");
		}
		else
		{
			this.amount -= amount;
			Console.WriteLine("Amount deducted successfully");
		}
	}
}
		

Now, the above changes will make our data member "amount" as private so it won't be accessible outside this "SavingsAccount" class. Also, no one would be able to assign a negative value in amount when calling the "SetAmount()" method. Also, if anyone wants to know the balance, he/she can call the "GetAmout()" method. So here the variable "amount" is safe of running in such issues. See let's modify our main method.


class Program
{
	static void Main(string[] args)
	{
		var aliceAccount = new SavingsAccount();
		aliceAccount.SetAmount(1000.00f);
		
		Console.WriteLine($"Alice's saving's account has total amount of {aliceAccount.GetAmount()}");
		
		aliceAccount.DeductAmount(1500.00f);
		
		Console.WriteLine($"Balance is now {aliceAccount.GetAmount()}");
	}
}
		

The above code will output "Cannot deduct the amount. Balance is not sufficient". So, we are safe here and the program is absolutely correct.

Now, before answering the original question (why use a property instead of variables?), I am asking you another question - What if there are multiple such data members? Well, then for each data member, we need to write and expose two methods - one is "Get___(...)" and the other is "Set___(...)". This will create a mess in our program and the user using the objects of the class will certainly not find it pleasant.

That's why C# Property comes into rescue. Properties give us the flexibility of using it as a variable and also having total control or validations on the property's data manipulation like methods.

The general syntax of property in C# is:


property-name 
{
	get 
	{
		// getter-logic
	}
	
	set
	{
		// setter-logic and assign value to the property using "value" keyword.
	}
}
		

where the "getter-logic" will return the original value or logic-driven value of the property and the "setter-logic" will assign a value to the property that can be exactly what was specified or can be logic-driven as well. Accessing the property is the same as accessing variables or methods of a class. To assign a value to the property, C# provides with a "value" keyword that is used to assign the value to the property.

Writing and consuming properties

Continuing with our above example, let's modify the program and write a property called "Amount" and implement the getter and setter logic for it.


class SavingsAccount
{
	float amount;
	
	public float Amount
	{
		get
		{
			return this.amount;
		}
		
		set
		{
			if(value > 0)
				this.amount = value;
		}
	}
	
	public void DeductAmount(float amount)
	{
		if(amount > this.amount)
		{
			Console.WriteLine("Cannot deduct amount. Balance is not sufficient");
		}
		else
		{
			this.amount -= amount;
			Console.WriteLine("Amount deducted successfully");
		}
	}
}
			

So, in the above program, we have removed the methods - "SetAmount()" and "GetAmount()" and implemented the same logic in our property "Amount". So, let's consume this in our main method.

Run this code


class Program
{
	static void Main(string[] args)
	{
		var aliceAccount = new SavingsAccount();
		aliceAccount.Amount = 1000.00f;
		
		Console.WriteLine($"Alice's saving's account has total amount of {aliceAccount.Amount}");
		
		aliceAccount.DeductAmount(1500.00f);
		
		Console.WriteLine($"Balance is now {aliceAccount.Amount}");
	}
}

/* Output:

Alice's saving's account has total amount of 1000
Cannot deduct the amount. Balance is not sufficient
Balance is now 1000

*/
			

See how clean our program became and it is signifying that it is more of a variable that is controlled behind the scenes. Properties are easy to remember over those getter and setter methods.

Read-only and write-only properties

The properties can be made read-only or write-only. To make a property read-only, we need to remove its "set" block and leaving the "get" block. The following property is a read-only property because we have removed its "set" block.


public float Amount
{
	get
	{
		return this.amount;
	}	
}
			

Now, trying to assign a value to this property will be a compile time error. The following code will give a compile-time error.


var aliceAccount = new SavingsAccount();
aliceAccount.Amount = 1000.00f;
			

Similarly, we can have write-only properties. To make a property write-only, we need to remove its "get" block and leaving the "set" block. The following property is a read-only property because we have removed its "get" block.


public float Amount
{
	set
	{
		if(value > 0)
		{
			this.amount = value;
		}
	}
}
			

Trying the access the above property will be a compile-time error.

Access modifiers in property accessors

Previously we have learned some of the access modifiers provided by C#. Those modifiers can be used with property accessors (get and set) as well. Consider the following example.


public float Amount
{
	get
	{
		return this.amount;
	}
	
	protected set
	{
		if(value > 0)
		{
			this.amount = value;
		}
	}
}
			

As you can see that we have made our accessor "set" as protected. This means that any class can access this property because it is public. However, the only child classes (inheritance) would be able to assign any value to this property because its "set" block has a "protected" access modifier.

Auto-implemented properties

In case there is no getter and/or setter logic is required in our properties then those properties should be declared more concisely with Auto-implemented property syntax. Following is an example of an auto-implemented property.


public float Amount { get; set; }
			

This precisely will act just as a variable to get and store the value of a specific data type. We usually do not expose variables. So as a good and clean code practice, if there is a need to expose any variable, then we usually do it with properties.

An auto-implemented property without "get" would be write-only auto-implemented property and without "set" it would be a read-only auto-implemented property.

Starting from C# 6.0, we can initialize auto-implemented properties as well just like variables. For e.g.


public string Age {get; set; } = 28;
			

Indexers

Indexers are a special type of properties that are indexed. Previously, we have talked about how variables can be replaced by properties that also gives more control over a variable in a way they are accessed and assigned with some value. Similarly, an Indexer makes the instances of a "class" or "struct" to be indexed like arrays in a more controlled way.

Indexers have a very specific use case but an important one. The general syntax of declaring an index is:


public data-type this[data-type key]
{
    get 
	{ 
		// getter-logic based on key
	}
    set 
	{
		// setter-logic based on key
	}
}
		

An "Indexer" very much used whenever we want to make our class as a collection class. Let's say we want to make a collection class say "HighSchoolStudents" and we have another class "Student" for individual students. So that when we create an object of "HighSchoolStudents" class (say "highSchoolStudents"), the indexer allows us to access any particular student by its enrolment number without writing any specific search logic outside the class.

Theoretically, it may be unclear to you. So, let's try to understand this with the example below.


class Student 
{
	public string Name {get;}
	public string Level {get;}
	public Student(string name, string level)
	{
		this.Name = name;
		this.Level = level;
	}
}

class HighSchoolStudents
{
	string[] enrolmentNumbers;
	int count = 0;
	int counter = 0;
	Student[] students;
	public HighSchoolStudents(int count)
	{
		this.count = count;
		enrolmentNumbers = new string[count];
		students = new Student[count];
	}
	
	public Student this[string enrolmentNumber]
	{
		get
		{
			var index = -1;
			for(var i=0; i < count; i++)
			{
				if(enrolmentNumbers[i] == enrolmentNumber)
				{
					index = i;
					break;
				}
			}
			
			if(index != -1)
			{
				return students[index];
			}
			else
			{
				return null;
			}
		}
		set
		{
			if(value.Level == "HighSchool")
			{
				enrolmentNumbers[counter] = enrolmentNumber;
				students[counter] = value;
				counter++;
			}
		}
	}
}
		

Now, in our main method, we can allocate some students to the High school whose levels are equal to "HighSchool".

Run this code


class Program
{
	static void Main(string[] args)
	{
		var highSchoolStudents = new HighSchoolStudents(5);
		highSchoolStudents["A1"] = new Student("Alice", "HighSchool");
		highSchoolStudents["A2"] = new Student("Bob", "HighSchool");
		highSchoolStudents["A3"] = new Student("Chris", "HighSchool");
		highSchoolStudents["A4"] = new Student("Dave", "HighSchool");
		highSchoolStudents["A5"] = new Student("Eric", "HighSchool");
		
		var a1 = highSchoolStudents["A1"];
		if(a1 == null) 
		{
			Console.WriteLine($"No student exists with the enrolment number a1");
		}
		else
		{
			Console.WriteLine($"High school student with enrolment number A1 is " + a1.Name);
		}		
	}
}
		

In the above main method, we can index the object "highSchoolStudents" by enrolment numbers like "A1", "A2", and so on. We could do so because the class "HighSchoolStudents" has an Indexer. We can clearly see how easy it is now to allocate a student in the high school and at the same time provide an enrolment number to them.

The class "HighSchoolStudents" maintains an initial array of "Student" type and in the "setter" we have written a logic to assign the student to a specific student array location. In the "getter", based on enrolment number we are simply iterating our "enrolmentNumbers" array to find the correct index using which we can retrieve the correct student from the "students" array.

As I already told indexers do have very specific use cases, so we need to choose it wisely while designing any C# application.

To Do

* Note : These actions will be locked once done and hence can not be reverted.

1. Track your progress [Earn 200 points]

2. Provide your ratings to this chapter [Earn 100 points]

0
Note : At the end of this chapter, there is a ToDo section where you have to mark this chapter as completed to record your progress.