What assignment copies
A class variable holds a reference to an object. After var other = original, both can refer to the same object. Changing an object field through other will be observable through original. Later assigning another object to other changes only that variable; it does not redirect original.
A struct variable contains a value. Copying a struct copies its fields. If a field is int, the copies can evolve separately. If a field references an array, the reference is copied and the array can remain shared. A struct does not automatically make everything reachable immutable.
In a rollback plan, a copy is an independent snapshot only if mutable data is also isolated. Before treating assignment as backup, identify reference fields and the writes that later processing can perform.
Strings: immutable contents, assignable variables
string is a reference type, but its contents are immutable. In string first = "queued"; string second = first; second += "!";, second receives the concatenation result. first continues to represent "queued". Object immutability does not mean that a local variable cannot receive another reference.
For operands typed as string, == compares contents. Both need not point to the same instance. ReferenceEquals asks a different question: do these references identify the same object? That can help diagnose sharing, but it does not define the identity of a customer code received in different messages.
Avoid generalizing string semantics to every class. A type can supply its own equality, and the selected operation also depends on expression types. Equality contracts and keys are developed further with objects and collections.
Nullability describes a contract
With Nullable=enable, string expresses an intended non-null reference; string? expresses permitted absence. The compiler tracks reference state and warns when usage can violate that intent. Both annotations still use System.String at runtime. This differs from int?, which is Nullable<int>.
An older library, external data or code ignoring warnings can still supply null. Changing a parameter annotation does not automatically add a guard at method entry. Treating warnings as errors changes the build result; it does not turn an annotation into a runtime check.
Postfix ! tells static analysis to accept a non-null state. It neither checks nor replaces the value. If string? token = null, token!.Length still dereferences null. Use a guard where the contract requires rejection; do not hide a diagnostic without a verifiable justification.
Conditional access and its limits
customer?.Name accesses Name only if customer exists. If customer is null, the expression supplies null. This rule does not suppress exceptions from an operation performed on an existing receiver. An array with one element still rejects values?[4]; ?[] does not turn an invalid index into absence.
In Boolean evaluation, && skips its second operand when the first is false. That permits code is not null && code.Length > 0. Operator & on bool evaluates both operands; it cannot replace that guard. Similarly, a null guard does not establish that contents satisfy other bounds.
C#14 permits conditional assignment on references, such as customer?.Name = BuildName(). If customer is null, the right-hand side is not evaluated at all. This can avoid optional work, but does not confirm that a required update occurred. When an application needs that confirmation, use an explicit branch and retain a success or failure result.
Boxing is not numeric conversion
Storing an int in object performs boxing: the object retains a copy of the int value. If the original variable changes from 15 to 16, the already boxed value remains 15. The value type inside that object is also part of the contract.
With object value = 15, attempting (long)value requests unboxing as long, but the object contains int. The attempt throws InvalidCastException. For this known origin, long count = (int)value first extracts int and then widens that value to long. checked does not change the boxed type.
This distinction appears in older object-based interfaces, heterogeneous collections and adapters. Before converting, establish whether the source is an already typed number, a boxed object or text. These are three different operations. If multiple source types are permitted, identify and handle authorized types; do not assume that every object displaying "15" has the same internal type.
Guided case: recognizing commands
An agent receives ASCII command codes. In this protocol, "SYNC", "sync" and "Sync" identify the same operation on every machine. Sorting names for display has a different purpose and can legitimately depend on linguistic rules. Server culture must not change this command’s identity.
The method below receives text that an earlier validation has already limited to ASCII when non-null. A null message is rejected. Contents are explicitly compared with OrdinalIgnoreCase. This avoids relying on current culture while preserving the rule accepting different casing. The method decides recognition only; it does not authenticate a sender or execute synchronization.
Correct comparison does not replace the rest of the contract. If commands later permit Unicode or surrounding spaces, those rules need definition. Do not add Trim or broad normalization merely to make an example pass: it might accept messages the protocol previously rejected.
Review sharing, presence and identity
Before changing an object, ask which other references can reach it. Before accessing a member, identify what establishes presence and which errors remain possible when the receiver exists. Before comparing text, choose whether the rule is object identity, exact contents or protocol-defined equality.
A production failure can begin with a small assumption: assignment mistaken for cloning, ! for validation, ?[] for exception handling or cultural comparison applied to an identifier. Each needs a different correction. Recording these premises in a review is more useful than merely removing the observed warning or exception.
Summary: copied references can share objects; structs can contain shared references; strings are immutable; nullable annotations and ! do not validate runtime input; unboxing requires compatibility; string comparison should express the contract.
static bool IsSync(string? command)
{
return command is not null
&& string.Equals(command, "SYNC",
System.StringComparison.OrdinalIgnoreCase);
}Read the method inside a class with nullable enabled. Assume prior ASCII validation for present values. null and "" give false; "Sync" gives true; " SYNC" gives false.
Common pitfalls
Confusing assignment with cloning; assuming a struct copies reachable objects; using ! to validate null; expecting ?[] to suppress invalid indices; unboxing through a different numeric type.
Related topics: Types, conversions and numeric boundaries · Methods, parameters and patterns · Objects, interfaces and records · Collections, generics and equality
Copying, presence and equality have different contracts; make each explicit in the decision.
Reference: Value types · C# 14 / .NET 10; no preview features