A subtype must be usable anywhere its base type is expected, without the caller needing to know or care which one it actually got. This is a stricter requirement than "compiles and doesn't throw" — it means the subtype can't narrow preconditions, widen postconditions, or violate any behavioral assumption a caller reasonably makes about the base type.
The textbook violation is Square inheriting from Rectangle:
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
// Looks reasonable in isolation — a square IS-A rectangle, mathematically.
public class Square : Rectangle
{
public override int Width
{
get => base.Width;
set { base.Width = value; base.Height = value; } // keeps it square
}
public override int Height
{
get => base.Height;
set { base.Width = value; base.Height = value; }
}
}
// This function is completely reasonable against the Rectangle contract...
void ResizeAndCheck(Rectangle r)
{
r.Width = 5;
r.Height = 4;
Debug.Assert(r.Area == 20); // passes for any real Rectangle...
// ...but FAILS for a Square, because setting Height silently changed
// Width too. The caller's reasonable assumption about Rectangle broke.
}
Square compiles, technically "is" a Rectangle, and still breaks every caller that assumes setting one dimension doesn't affect the other. The fix is usually to recognize that the inheritance relationship was wrong in the first place — Square and Rectangle should not be in an inheritance relationship at all if their contracts genuinely differ this much; a shared IShape interface with just an Area property, implemented independently by each, avoids the trap entirely.
public interface IShape
{
int Area { get; }
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area => Width * Height;
}
public class Square : IShape
{
public int Side { get; set; }
public int Area => Side * Side;
}