Chapter 4: Operators and Conversions
Operators are the building blocks of expressions in C#. They combine values, variables, and method calls to produce new values. Conversions change a value from one type to another; some are automatic, some require an explicit cast. This chapter explains common operators, how expressions are evaluated, how overflow is handled, and how to add your own operators in custom types.
C# Operators
Arithmetic operators work with numeric types; comparison operators evaluate to a bool; logical operators combine or invert bool values. C# also includes bitwise operators for integral types. Each group has predictable rules and edge cases worth knowing.
The Arithmetic operators: +, -, *, /, %
As you would expect + adds, - subtracts, * multiplies, / divides, and % gives the remainder. For integer types, / truncates toward zero; for floating-point types it performs real division. The remainder operator works with integers and floating-point types, although floating-point remainders can be surprising when values include rounding error.
int a = 7, b = 3;
int q = a / b; // 2
int r = a % b; // 1
double x = 7, y = 3;
double d = x / y; // 2.33333333333333…
double and float. Prefer comparisons within a tolerance.
The increment, decrement and unary operators: ++, --, +, -
Increment and decrement adjust integral or floating-point values by one. Prefix form updates then yields the updated value; postfix yields the original value then updates. Unary - negates; unary + is a no-op used for readability.
int n = 5;
int a1 = ++n; // n = 6, a1 = 6
int a2 = n--; // n = 5, a2 = 6
The comparison operators: ==, !=, <, <=, >, >=
Comparison operators return true or false. For numeric types the meaning is straightforward. For reference types, == and != may be overloaded to compare values; otherwise they compare references. The string type defines value semantics for ==, so two distinct instances with the same contents compare as equal.
string s1 = new string("hello");
string s2 = new string("hello");
bool eq = (s1 == s2); // true (string value equality)
The logical operators: &&, ||, ! (short-circuit logical)
&& evaluates the right-hand side only if the left-hand side is true; || evaluates the right-hand side only if the left-hand side is false. ! negates a boolean value. Short-circuiting is essential for guarding computations that would otherwise throw exceptions.
if (s != null && s.Length > 0) Console.WriteLine(s[0]);
The bitwise operators: &, |, ^, ~, <<, >>
Bitwise operators act on integral types and bool. &, |, and ^ combine bits; ~ inverts bits; << and >> shift left or right. On bool, & and | are non-short-circuiting logical operations.
int flags = 0b_0010;
flags |= 0b_1000; // set bit 3
bool both = true & ExpensiveCheck(); // always calls ExpensiveCheck()
& and | on bool do not short-circuit; prefer && and || when you intend short-circuit logic.
Assignment and compound assignment
Assignment uses = to store the right-hand value into the left-hand variable or property. Compound assignment applies an operator and assigns in one step; it can improve readability and sometimes performance with properties or indexers.
The = operator and basic compound forms
Every compound operator op= corresponds to an operator op; examples include +=, -=, *=, /=, %=, &=, |=, ^=, <<=, and >>=.
int total = 0;
total += 5; // total = 5
total *= 2; // total = 10
The Null-coalescing and assignment operators: ??, ??=
expr1 ?? expr2 yields expr1 when it is non-null; otherwise it yields expr2. x ??= y assigns y to x only when x is null, then yields the resulting value.
string? name = null;
name ??= "Anonymous"; // "Anonymous"
var display = name ?? "Guest";
Deconstruction and tuple assignment
Tuples support parallel assignment and deconstruction into existing variables or placeholders. Use _ for fields you intend to ignore.
(int x, int y) = (3, 4);
(int _ , int top) = GetBounds(); // ignore the first value
Operator precedence and associativity
Precedence determines which operators bind more tightly in expressions; associativity determines how operators of the same precedence group when there are no parentheses. When in doubt, use parentheses for clarity; the compiler and your readers benefit.
Selected precedence overview
The following table lists common groups in order from higher to lower precedence. Items on the same row associate as shown.
| Group | Examples | Associativity |
| Primary | x.y, x[y], f(…), new, checked |
Left to right |
| Unary | +, -, !, ~, ++, --, await, sizeof, typeof |
Right to left |
| Multiplicative | *, /, % |
Left to right |
| Additive | +, - |
Left to right |
| Shifts | <<, >> |
Left to right |
| Relational | <, <=, >, >=, is, as |
Left to right |
| Equality | ==, != |
Left to right |
| Logical AND | && |
Left to right |
| Logical OR | || |
Left to right |
| Null-coalescing | ?? |
Right to left |
| Conditional | cond ? a : b |
Right to left |
| Assignment | =, +=, *=, &=, ??=, … |
Right to left |
A tight operator like * binds before a looser operator like +. Therefore 2 + 3 * 4 is 14, not 20. Parentheses always make intent explicit.
Checked and unchecked arithmetic
Overflow in integral arithmetic wraps around by default in unchecked context. A checked context raises OverflowException on overflow for integral arithmetic and integral conversions. You can apply checked or unchecked to an expression or a block.
int x = int.MaxValue;
int wrap = x + 1; // wraps to int.MinValue (unchecked by default)
int fail = checked(x + 1); // throws OverflowException
checked {
byte b = 255;
b += 1; // OverflowException
}
checked affects integral arithmetic and conversions; it does not affect float or double which follow IEEE rules and never throw on overflow.
Implicit and explicit conversions
An implicit conversion is applied automatically when it is guaranteed to succeed without data loss; an explicit conversion requires a cast because it can fail or lose information. C# also supports user-defined conversions using implicit and explicit operators on your types.
Numeric widening and narrowing
Widening conversions move to a type that can represent every value of the source type; narrowing conversions risk overflow or precision loss. Widening is implicit; narrowing requires a cast.
int i = 123;
long L = i; // implicit (widening)
short s = (short)i; // explicit (narrowing)
Reference, nullable, and enum conversions
Reference conversions include upcasts within an inheritance chain and interface conversions; these are implicit. Downcasts require a cast and can fail at runtime. Nullable conversions add or remove ? while preserving or unwrapping the underlying value. Enums convert to and from their underlying integral type with explicit casts.
Stream s = new MemoryStream(); // implicit upcast
MemoryStream m = (MemoryStream)s; // explicit downcast, may throw if not a MemoryStream
int? nx = 42;
int ny = nx ?? 0; // coalesce to non-nullable
enum Color : byte { Red = 1, Green = 2 }
byte b = (byte)Color.Red; // explicit to underlying type
as, pattern matching, and safe casts
as performs a reference or nullable conversion; it yields null on failure rather than throwing. Pattern matching with is can test and capture the converted value in one step.
object o = "hi";
var ms = o as MemoryStream; // null (no exception)
if (o is string s1 && s1.Length > 0) Console.WriteLine(s1);
Parsing and formatting
Converting between text and numbers is not an operator feature; it is very common in real programs. Use TryParse to avoid exceptions when input may be invalid.
if (int.TryParse("123", out int v)) Console.WriteLine(v);
string formatted = $"{DateTime.UtcNow:O}";
User-defined operators
Structs and classes can overload many operators by declaring public static methods with the operator keyword. You can also define user-defined conversions using implicit or explicit. Choose overloading when it makes code clearer; avoid surprising semantics.
Overloading arithmetic and comparison
Overloadable operators include arithmetic, bitwise, equality, and ordering operators. Some operators must be provided in pairs; for example if you overload == you should also overload !=. For ordering, provide < and > together and usually <= and >= as well.
public readonly struct Meters
{
public double Value { get; }
public Meters(double v) => Value = v;
public static Meters operator +(Meters a, Meters b) => new Meters(a.Value + b.Value);
public static Meters operator -(Meters a, Meters b) => new Meters(a.Value - b.Value);
public static bool operator ==(Meters a, Meters b) => a.Value == b.Value;
public static bool operator !=(Meters a, Meters b) => !(a == b);
public override bool Equals(object? obj) => obj is Meters m && m.Value == Value;
public override int GetHashCode() => Value.GetHashCode();
}
Meters to Seconds unless you define a clear result type.
User-defined conversions
Conversion operators live on the source or target type. Use implicit only when the conversion cannot lose information or surprise readers. Prefer explicit when the conversion may fail or is narrowing.
public readonly struct Ratio
{
public double Value { get; }
public Ratio(double v) => Value = v;
public static implicit operator double(Ratio r) => r.Value;
public static explicit operator Ratio(double v) => new Ratio(v);
}
True/false and custom truthiness
Types can define operator true and operator false so instances can participate in if and conditional operators. This is rare; most types should not define custom truthiness because it can reduce clarity.
public readonly struct Validated
{
public bool IsValid { get; }
public Validated(bool ok) => IsValid = ok;
public static bool operator true(Validated v) => v.IsValid;
public static bool operator false(Validated v) => !v.IsValid;
}
Minimal class outline with operators
The following sketch shows where operator members live within a type; bodies omitted for brevity with … to indicate elided content.
public sealed class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) { Amount = amount; Currency = currency; }
public static Money operator +(Money a, Money b) => … // validate currency then add
public static bool operator ==(Money a, Money b) => …
public static bool operator !=(Money a, Money b) => …
public static explicit operator decimal(Money m) => m.Amount;
public override bool Equals(object? o) => …
public override int GetHashCode() => …
}