Does string? change anything at runtime?
No. For reference types, string? and string are the same runtime type, System.String. The ? is an annotation for the compiler's static analysis, stored as metadata attributes. Nothing stops a null from reaching a string parameter at runtime.
Question
Does string? change anything at runtime?
What It Is
Nullable reference types (enabled by default in new projects since .NET 6, via <Nullable>enable</Nullable>) let you declare intent: string means "should never be null", string? means "may be null". The compiler then tracks the null state of every variable through your code and warns when you dereference something that might be null, or assign null to a non-nullable.
The result is compiled into [Nullable] and [NullableContext] attributes so other assemblies can read your annotations. But the IL types are unchanged.
The Contrast with `int?`
This is easy to confuse, because the syntax is identical:
int?isNullable<int>, a different struct with aHasValueflag. It really exists at runtime.string?is juststringplus a compile-time hint.
So nullable reference types are warnings, not guarantees. Reflection, deserializers, older libraries without annotations, or the ! (null-forgiving) operator can all put a null where the compiler thinks there is none.
Practical Example
csharp#nullable enable
string Greet(string name) => $"Hello, {name.ToUpper()}";
string? maybe = null;
Greet(maybe); // warning CS8604: possible null reference argument
Greet(maybe!); // no warning, but NullReferenceException at runtime
// Same runtime type:
string? a = "x";
string b = "y";
Console.WriteLine(a.GetType() == b.GetType()); // True: both are System.String
// Public APIs still need runtime checks
public void Register(string name)
{
ArgumentNullException.ThrowIfNull(name); // callers may have nullable disabled
}Solution and Conclusion
Treat nullable reference types as a powerful linter, and make it strict with <WarningsAsErrors>nullable</WarningsAsErrors>. Use ! only when you know something the compiler can't see. At public boundaries (public APIs, deserialized input), keep real runtime checks like ArgumentNullException.ThrowIfNull.