Object-oriented principles restrict direct access to a class's internal state. C# employs properties—defined via get and set accessors—to mediate interactions with private fields. This mechanism acts as a controlled gateway, preserving encapsulation while exposing necessary data.
Consider a class maintaining internal state:
class Worker
{
private string fullName;
private int yearsExperience;
public string FullName
{
get { return fullName; }
set { fullName = value; }
}
public int YearsExperience
{
get { return yearsExperience; }
set { yearsExperience = value; }
}
}The general structure for declaring a property requires matching the return type to the backing field, utilizing a distinct name:
public ReturnType PropertyName
{
get { return BackingField; }
set { BackingField = value; }
}Retrieving a property invokes the get accessor, while assigning a value triggers the set accessor. Omitting the set block renders the property read-only, and excluding get makes it write-only.
Relying on properties instead of public fields permits runtime validation. If an invalid value is assigned, the set accessor can reject it:
class Worker
{
private string fullName;
private int yearsExperience;
public string FullName
{
get { return fullName; }
set { fullName = value; }
}
public int YearsExperience
{
get { return yearsExperience; }
set
{
if (value >= 0 && value <= 50)
{
yearsExperience = value;
}
}
}
}Properties also facilitate transparent data transformation without altering the external interface. A heat sensor might internally store Kelvin but externally report Celsius:
class HeatSensor
{
private double kelvinValue;
public HeatSensor(double initialKelvin)
{
kelvinValue = initialKelvin;
}
public double CurrentTemp
{
get { return kelvinValue - 273.15; }
set { kelvinValue = value + 273.15; }
}
}Invoking new HeatSensor(313.15).CurrentTemp yields 40. If the underlying storage shifts, the public contract remains intact.
Exposing a field directly as a public member circumvents encapsulation:
public class User1
{
public string identifier;
}Using auto-implemented properties enforces encapsulation even without custom logic:
public class User2
{
public string Identifier { get; set; }
}The compiler generates a hidden backing field for Identifier, ensuring the architecture supports future validation additions without breaking consuming code. Manually expanding an auto-property looks like this:
private string identifier;
public string Identifier
{
get { return identifier; }
set { identifier = value; }
}Custom logic within accessors handles null checks or formatting:
private string identifier;
public string Identifier
{
get { return identifier; }
set { identifier = string.IsNullOrEmpty(value) ? "Unknown" : value; }
}Validation ensures state integrity:
class Profile
{
private int userAge;
public int UserAge
{
get { return userAge; }
set
{
if (value < 0 || value > 150)
{
userAge = 0;
}
else
{
userAge = value;
}
}
}
}Accessors provide three core capabilities: concealing internal representation, enforcing business rules, and triggering side effects upon state mutation.
For scenarios requiring no custom logic, C# offers concise syntax. Traditional implementations:
class Configuration
{
private int timeout;
public int Timeout
{
get { return timeout; }
set { timeout = value; }
}
}Expression-bodied members:
class Configuration
{
private int timeout;
public int Timeout
{
get => timeout;
set => timeout = value;
}
}Auto-implemented properties eliminate the need for explicit backing fields:
class Configuration
{
public int Timeout { get; set; }
}