Classes: properties, constructors and ToString
Defining your own kind of value, properties that read and write safely, a constructor that runs at creation, and the text an object prints.
A shape you define yourself
The built-in types cover a single value each. A class lets you name a shape made of several values, so the program can talk about an item, an order or a player instead of a loose bag of variables. class Item { ... } declares the shape, and new Item("Tea", 4.5m) builds one of them, exactly the way new List<string>() does.
Once declared, the class name is a type like any other. It can be the type of a variable, the type of a method parameter, or the type inside a list: List<Item> is a list of items. That is the whole point of the next few chapters.
Properties read and write
A property looks like a value from the outside and is a method inside. The short form, public string Name { get; set; }, declares both a place to keep the value and the two ways in: get to read it, set to write it. Reading item.Name looks like reading a field, and calling item.Name = "Jam" looks like writing one.
The word public decides who may use it, and there are good reasons to be stricter. public decimal Price { get; private set; } lets anyone read the price while only the class itself may change it, which is how a value that should be settled once stays settled. The general habit: keep the writable surface as small as the program allows.
A constructor runs at creation
A constructor is the method that runs when new is used. It carries the name of the class and no return type, and its parameters are what the creator has to supply: public Item(string name, decimal price). Assigning the parameters to the properties there is what turns a few arguments into a value the rest of the program can rely on.
Every object also has a ToString, inherited from object. Overriding it decides what printing the object shows — Console.WriteLine(item) calls it for you. That single decision is worth making, because the alternative is a printed line naming the class and nothing useful about the value.
C# also has a shorter way to fill an object when a class allows it: new followed by the type and a pair of braces holding property assignments. It calls a constructor with no arguments and then sets the properties by name, which reads well when a class has many optional parts. It only works when the properties are settable and there is a parameterless constructor, so the Item above cannot be built this way: its only constructor asks for both values, and Price has a private setter. The longer form is the one that keeps the guarantee.
An item, with a property that stays put
using System;
class Item
{
// An auto-property: storage with a getter and a setter.
public string Name { get; set; }
// Readable by anyone, writable only inside this class.
public decimal Price { get; private set; }
// The constructor runs when new is used.
public Item(string name, decimal price)
{
Name = name;
Price = price;
}
// Printing the object shows this, not the class name.
public override string ToString()
{
return Name + " " + Price.ToString("F2");
}
}
class Program
{
static void Main()
{
var item = new Item("Tea", 4.5m);
item.Name = "Green tea";
Console.WriteLine(item); // Green tea 4.50
Console.WriteLine(item.Name); // Green tea
Console.WriteLine(item.Price); // 4.50
}
}A value with a name and a price
Run it and read the two lines. Then change the name after the object is built, print the object again, and try the same with the price to see what the private setter refuses.
A class that prints itself well is easier to debug: one Console.WriteLine(item) replaces four lines joining its parts by hand.
A reference is a name for an object
new Item(...) builds an object somewhere in memory and hands back a reference, a small value that points at it. var item = ... stores that reference in a variable. A second assignment, var other = item;, copies the reference, so both names now lead to the same object.
That is why changing a property through one name is visible through the other, and why comparing two objects with == asks whether they are the same object, not whether their properties match. Two new Item("Tea", 4.5m) calls make two different objects even when every value inside them is equal.
A constructor protects an invariant
An invariant is something that must be true for the object to make sense: a price is not negative, a name is not empty, a start date is not after an end date. The constructor is the one place every object must pass through, so it is the right place to check.
A property with a private setter keeps the value from being changed after that check. The alternative, a public setter and a validation method nobody calls, leaves the object able to hold nonsense, and every method that reads it has to wonder.
Three class mistakes: calling a member on a variable that is still null, which throws NullReferenceException at the moment of the call; expecting == to compare two objects by their contents when it compares references; and forgetting new, so a declaration creates a name but no object.
Try these next
- Make two variables point at one item, change the name through one, and print it through the other.
- Give a class a constructor that refuses an empty name, then try to create one and read the exception.
- Override
ToStringon a class, print the object directly, and then print it inside a sentence with interpolation.
Two names for one counter
Run it and read the five lines. Then add a third variable that points at the same counter, add ten through it, and print the first variable again.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does a class do that a list cannot?
