On this page
Classes and objects in C# 14
Classes in C#
A class is a blueprint for creating objects. It defines the structure (properties/fields) and the behavior (methods) that instances created from it will have.
// Minimal class definition
public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
// Creating instances
var p1 = new Person { Name = "David", Age = 30 };
var p2 = new Person();
p2.Name = "Maria";
p2.Age = 25;Access modifiers
Access modifiers control from where a member can be accessed:
| Modifier | Accessible from |
|---|---|
public |
Anywhere |
private |
Only within the same class |
protected |
Class + derived classes |
internal |
Within the same assembly |
protected internal |
Assembly or derived classes |
private protected |
Same class + derived in same assembly |
By default, class members are private and classes are internal.
Properties
Properties in C# encapsulate fields and allow adding validation:
public class BankAccount
{
// Auto-implemented property — the compiler generates the backing field
public string Owner { get; set; } = string.Empty;
// Property with custom validation
private decimal _balance;
public decimal Balance
{
get => _balance;
private set // can only be set from within the class
{
if (value < 0)
throw new InvalidOperationException("Balance cannot be negative");
_balance = value;
}
}
// Computed property (no backing field)
public bool HasBalance => _balance > 0;
// init-only property — assignable only during construction
public string AccountNumber { get; init; } = Guid.NewGuid().ToString();
public BankAccount(string owner, decimal initialBalance)
{
Owner = owner;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Amount must be positive");
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > Balance) throw new InvalidOperationException("Insufficient funds");
Balance -= amount;
}
}Constructors
A constructor initializes the object when it is created:
public class Connection
{
public string Host { get; }
public int Port { get; }
public bool UseSsl { get; }
// Primary constructor
public Connection(string host, int port, bool useSsl = true)
{
Host = host;
Port = port;
UseSsl = useSsl;
}
// Convenience constructor — calls the primary with :this()
public Connection(string host) : this(host, 5432) { }
// Static constructor — runs once when the type is loaded
static Connection()
{
Console.WriteLine("Connection type initialized");
}
}
var conn1 = new Connection("localhost", 1433, false);
var conn2 = new Connection("db.example.com"); // uses port 5432 and SSLPrimary constructors (C# 12+)
The most concise syntax for simple classes:
// Without primary constructor (classic C#)
public class ClassicPoint
{
public double X { get; }
public double Y { get; }
public ClassicPoint(double x, double y) { X = x; Y = y; }
}
// With primary constructor (C# 12+)
public class Point(double x, double y)
{
public double X { get; } = x;
public double Y { get; } = y;
// Parameters x and y are available throughout the class body
public double DistanceFromOrigin() => Math.Sqrt(x * x + y * y);
public override string ToString() => $"({x:F2}, {y:F2})";
}
var p = new Point(3.0, 4.0);
Console.WriteLine(p); // (3.00, 4.00)
Console.WriteLine(p.DistanceFromOrigin()); // 5Static members
Static members belong to the class, not to instances:
public class Counter
{
private static int _total = 0;
public static int Total => _total;
public int Id { get; }
public Counter()
{
_total++;
Id = _total;
}
// Static factory method
public static Counter Create() => new Counter();
// Static utility method
public static void Reset() => _total = 0;
}
var c1 = Counter.Create();
var c2 = Counter.Create();
var c3 = Counter.Create();
Console.WriteLine(Counter.Total); // 3Object initializers
The {} syntax to initialize properties without an explicit constructor:
public class PostalAddress
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Country { get; set; } = "United States";
public string PostalCode { get; set; } = string.Empty;
}
// Object initializer
var addr = new PostalAddress
{
Street = "123 Main St",
City = "New York",
// Country uses the default "United States"
PostalCode = "10001"
};
// init-only — assignable only in initializer, immutable after
public class Config
{
public string ApiUrl { get; init; } = string.Empty;
public int Timeout { get; init; } = 30;
public bool Debug { get; init; }
}
var cfg = new Config { ApiUrl = "https://api.example.com", Timeout = 60 };
// cfg.Timeout = 90; // ← Compile error: init-only propertyPartial classes
Allow splitting a class across multiple files:
// File: Order.cs
public partial class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
public string Customer { get; set; } = string.Empty;
}
// File: Order.Validation.cs
public partial class Order
{
public bool IsValid() => Total > 0 && !string.IsNullOrEmpty(Customer);
public void Validate()
{
if (!IsValid())
throw new InvalidOperationException("Invalid order");
}
}sealed and abstract classes
// sealed — cannot be inherited
public sealed class Singleton
{
private static readonly Singleton _instance = new();
public static Singleton Instance => _instance;
private Singleton() { }
}
// abstract — cannot be instantiated directly
public abstract class Shape
{
public abstract double Area();
public abstract double Perimeter();
// Concrete method available to subclasses
public string Describe() => $"Area: {Area():F2}, Perimeter: {Perimeter():F2}";
}Practice
- BankAccount class: Implement a class with validated
Balance(non-negative),Owner, and an init-onlyAccountNumber. AddDepositandWithdrawmethods with proper validation. - Primary constructor: Rewrite
Point(double x, double y)with a primary constructor and add aMoveTo(double dx, double dy)method that returns a newPoint. - Static member: Add a static counter to your
BankAccountclass to track how many accounts have been created.
In the next lesson we will look at inheritance, abstract classes, interfaces, and how C# implements polymorphism.
// Full class with all modern C# 14 features
public class Product
{
// Auto-implemented properties
public int Id { get; init; } // init: only in constructor/initializer
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
// Computed property (no backing field)
public decimal PriceWithTax => Price * 1.13m;
// Private field with validated property
private int _stock;
public int Stock
{
get => _stock;
set
{
if (value < 0) throw new ArgumentException("Stock cannot be negative");
_stock = value;
}
}
// Static member
public static int TotalCreated { get; private set; }
// Constructor
public Product(int id, string name, decimal price, int stock)
{
Id = id;
Name = name;
Price = price;
Stock = stock;
TotalCreated++;
}
// Override ToString
public override string ToString()
=> $"[{Id}] {Name} — ${Price:F2} (stock: {Stock})";
}
// Primary constructor (C# 12+)
public class Category(int id, string name)
{
public int Id { get; } = id;
public string Name { get; } = name;
public override string ToString() => $"Category: {Name}";
}
// Usage
var prod = new Product(1, "Laptop Pro", 1299.99m, 10);
Console.WriteLine(prod);
Console.WriteLine($"With tax: ${prod.PriceWithTax:F2}");
Console.WriteLine($"Products created: {Product.TotalCreated}");
var cat = new Category(1, "Electronics");
Console.WriteLine(cat);
Sign in to track your progress