🤖 Classes and Objects - Part 3
Building on our understanding of classes, objects, encapsulation, constructors, and properties, this lesson explores further refinements and features related to class design in C#. These concepts help write more concise, efficient, and powerful object-oriented code.
We will cover:
static
Members: Fields and methods belonging to the class itself, not individual objects.- The
this
Keyword: Referencing the current object instance. - Auto-Implemented Properties: A shorthand syntax for simple properties.
- Expression-Bodied Members: Concise syntax for simple methods and properties.
- Object Initializers: An alternative syntax for setting properties during object creation.
(Prerequisites: Ensure you understand the concepts from the previous “Classes and Objects” posts.)
Lesson: Refining Class Design
static
Members: Shared by All Objects
So far, the fields and methods we’ve defined (like _name
or Deposit()
) belong to individual objects. Each Player
object has its own _name
, and calling Deposit()
on one BankAccount
object affects only that object’s balance.
Sometimes, however, you need data or functionality related to the class itself, rather than any specific instance. For this, we use the static
keyword.
static
Fields: A single copy of the field exists, shared across all objects of the class (and accessible even if no objects exist). Useful for constants shared by all instances or for keeping track of class-level information (like how many objects have been created).static
Methods: Methods called directly on the class, not on an object instance. They cannot directly access instance fields or methods (like_name
orDeposit()
) because they don’t operate on a specific object. They often work with static fields or operate solely on their input parameters.Console.WriteLine()
andMath.Max()
are examples of static methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Circle
{
// Instance field (each circle has its own radius)
private double _radius;
// Static field (shared constant for all circles)
public const double Pi = 3.14159; // Constants are implicitly static
// Static field (shared counter for all circles)
private static int _numberOfCirclesCreated = 0;
// Constructor (instance method)
public Circle(double radius)
{
_radius = radius;
_numberOfCirclesCreated++; // Increment the static counter
}
// Instance method (operates on a specific circle's radius)
public double CalculateArea()
{
return Pi * _radius * _radius; // Can access static Pi and instance _radius
}
// Static method (belongs to the Circle class, not an instance)
public static int GetNumberOfCirclesCreated()
{
// Cannot access _radius here because it belongs to an instance
// Console.WriteLine(_radius); // Error!
return _numberOfCirclesCreated; // Can access static field
}
}
// --- Usage ---
Console.WriteLine($"Value of Pi: {Circle.Pi}"); // Access static constant via class name
Circle c1 = new Circle(5.0);
Circle c2 = new Circle(10.0);
Console.WriteLine($"Area of c1: {c1.CalculateArea()}"); // Call instance method on object
// Access static method via class name
Console.WriteLine($"Number of circles created: {Circle.GetNumberOfCirclesCreated()}"); // Output: 2
static
is useful for utility methods, constants, factory methods, or managing class-level state.
The this
Keyword: Referring to the Current Instance
Inside an instance method or constructor, you sometimes need to refer explicitly to the current object the method is being called on. The this
keyword provides this reference.
Common uses:
- Disambiguation: When a parameter name is the same as a field name.
- Passing the Current Object: Passing the current object as an argument to another method.
- Constructor Chaining: Calling one constructor from another within the same class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Rectangle
{
private double _width;
private double _height;
// Constructor using 'this' for disambiguation
public Rectangle(double width, double height)
{
this._width = width; // 'this._width' is the field, 'width' is the parameter
this._height = height; // 'this._height' is the field, 'height' is the parameter
}
// Constructor chaining: Call the first constructor with default values
public Rectangle() : this(1.0, 1.0) // Calls Rectangle(1.0, 1.0)
{
Console.WriteLine("Default rectangle created.");
}
public double CalculateArea()
{
return _width * _height;
}
// Example of passing 'this'
public void PrintDetails(Printer printer)
{
printer.Print(this); // Pass the current Rectangle object to the Printer
}
}
// Dummy Printer class for example
public class Printer
{
public void Print(Rectangle rect) { /* ... print rect details ... */ }
}
While not always strictly necessary (the compiler often infers it), this
can improve clarity, especially when dealing with naming conflicts or constructor chaining.
Auto-Implemented Properties: Less Boilerplate
Often, a property simply gets and sets a private backing field without any extra logic. C# provides a shorthand syntax called auto-implemented properties for this common case.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Product
{
// Auto-Implemented Property for Name
// Compiler automatically creates a hidden private backing field.
public string Name { get; set; }
// Auto-Implemented Property for Price (with private set)
public decimal Price { get; private set; }
// Constructor to set initial values
public Product(string name, decimal price)
{
Name = name;
Price = price; // Can use the private set from within the class
}
// You can still add methods
public void ApplyDiscount(decimal percentage)
{
if (percentage > 0 && percentage < 1)
{
Price *= (1 - percentage); // Modify using the private set
}
}
}
// --- Usage ---
Product laptop = new Product("Laptop", 1200.00m);
Console.WriteLine($"Product: {laptop.Name}, Price: {laptop.Price:C}");
laptop.Name = "Gaming Laptop"; // Use the public set
// laptop.Price = 1100.00m; // Error! Price has a private set.
laptop.ApplyDiscount(0.10m); // Price is changed internally
Console.WriteLine($"Updated Price: {laptop.Price:C}");
Use auto-properties when you don’t need custom logic in the get
or set
accessors. You can always expand them later if needed.
Expression-Bodied Members: Concise Syntax
For methods, constructors, or property accessors that consist of only a single expression or statement, C# offers a more concise syntax using =>
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Point
{
public double X { get; }
public double Y { get; }
// Constructor using expression body
public Point(double x, double y) => (X, Y) = (x, y);
// Read-only property using expression body
public double DistanceFromOrigin => Math.Sqrt(X * X + Y * Y);
// Method using expression body
public void Translate(double dx, double dy) => (X, Y) = (X + dx, Y + dy); // Note: This won't compile as X,Y are readonly. Example syntax only.
// Method returning a value using expression body
public override string ToString() => $"({X}, {Y})";
}
This syntax can make simple members much shorter and sometimes easier to read.
Object Initializers: Setting Properties on Creation
Besides using constructors, you can also set the values of accessible properties immediately after creating an object using object initializer syntax.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string City { get; set; }
}
// --- Usage ---
// Using default constructor and object initializer
Customer customer1 = new Customer
{
CustomerId = 1,
Name = "Bob",
City = "New York" // Properties must be settable (have a public set)
};
Console.WriteLine($"Customer: {customer1.Name} from {customer1.City}");
This syntax is convenient when you have multiple properties to set and don’t have (or don’t want to use) a constructor that takes all those values.
Tutorial: Static Utility Class - Calculator
Let’s create a simple utility class with only static
methods.
Objective: Create a Calculator
class with static methods for basic arithmetic operations.
Prerequisites: A C# console project.
Step 1: Define the Calculator
Class
Create Calculator.cs
. Since this class will only contain static methods and doesn’t need to be instantiated, we can also mark the class itself as static
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Calculator.cs
using System;
public static class Calculator // Static class
{
// Static method for Addition
public static double Add(double a, double b)
{
return a + b;
}
// Static method for Subtraction
public static double Subtract(double a, double b)
{
return a - b;
}
// Static method for Multiplication (using expression body)
public static double Multiply(double a, double b) => a * b;
// Static method for Division (with check for zero)
public static double Divide(double a, double b)
{
if (b == 0)
{
Console.WriteLine("Error: Cannot divide by zero.");
return double.NaN; // Not a Number indicates failure
}
return a / b;
}
}
Explanation: A static
class cannot be instantiated (new Calculator()
is illegal) and can only contain static
members. We define static methods for the operations.
Step 2: Use the Calculator
Class in Program.cs
Call the static methods directly using the class name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Program.cs
using System;
public class Program
{
public static void Main()
{
double num1 = 10.5;
double num2 = 2.5;
// Call static methods using the class name
double sum = Calculator.Add(num1, num2);
double difference = Calculator.Subtract(num1, num2);
double product = Calculator.Multiply(num1, num2);
double quotient = Calculator.Divide(num1, num2);
double invalidQuotient = Calculator.Divide(num1, 0);
Console.WriteLine($"{num1} + {num2} = {sum}");
Console.WriteLine($"{num1} - {num2} = {difference}");
Console.WriteLine($"{num1} * {num2} = {product}");
Console.WriteLine($"{num1} / {num2} = {quotient}");
Console.WriteLine($"{num1} / 0 = {invalidQuotient}"); // Will show NaN after error message
}
}
Step 3: Build and Run
Save both files, build, and run. Observe how the static methods are called directly on the Calculator
class.
Exercise: Configurable Logger
Class
Let’s create a Logger
class that uses static members for configuration and instance members for logging messages, incorporating auto-properties and the this
keyword.
Project Goal: Create a Logger
class where you can set a static logging level, and instances of the logger prepend a specific prefix to messages.
Requirements:
- Create a new console project.
- Define a
Logger
class (Logger.cs
). - Add a
static
propertyLogLevel
of typeint
(e.g., 0=Debug, 1=Info, 2=Warning, 3=Error). Use an auto-property with a publicget
and a publicset
. Initialize it to 1 (Info) by default. - Add a private instance field
_prefix
of typestring
. - Add a public constructor that takes a
string prefix
parameter. Use thethis
keyword to assign the parameter to the_prefix
field. - Add a public instance method
Log(string message, int messageLevel)
.- This method should only print the message if
messageLevel
is greater than or equal to the staticLogLevel
. - If it prints, the output should be formatted as:
[PREFIX]: [Message]
(e.g.,[WebApp]: User logged in
).
- This method should only print the message if
- In
Program.cs
:- Create two different
Logger
instances with different prefixes (e.g.,new Logger("[WebApp]")
,new Logger("[Database]")
). - Log messages using different levels with both loggers.
- Change the static
Logger.LogLevel
and log messages again to see the filtering effect.
- Create two different
Hints:
- Static auto-property:
public static int LogLevel { get; set; } = 1;
- Accessing static property inside instance method:
if (messageLevel >= Logger.LogLevel)
- Constructor:
public Logger(string prefix) { this._prefix = prefix; }
Steps:
- Implement the
Logger
class. - Implement the test code in
Program.cs
. - Save, build, and run.
- Verify that messages are filtered based on the static
LogLevel
and that each logger uses its correct prefix.
Conclusion
This lesson introduced powerful C# features for class design: static
members for class-level data and behavior, the this
keyword for instance referencing and constructor chaining, auto-implemented properties and expression-bodied members for concise syntax, and object initializers for flexible object creation. Using these tools effectively allows you to write cleaner, more maintainable, and more expressive object-oriented code.