Skip to content

C# and .NET docs supplement

James Groom edited this page Jul 23, 2024 · 33 revisions

To save us repeating our complaints about the lack of proper documentation under each section, let's agree to gather all the frustration here:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Contribute to the official docs if possible.

BCL source

The API reference on Microsoft Learn (formerly MSDN) now links to the source in most places, but of course that's for modern .NET.

Const (byte/primitive) arrays

Not allowed as either array nor Span, despite string literals now effectively having the type const ReadOnlySpan<char>, and despite arrays of primitive types being allowed for attribute parameters since forever. Use static readonly and weep.

Const structs

Not allowed, even if they meet the criteria for unmanaged types and are littered with explicit layout attributes. First-class'd structs are no different, so fields of type ValueTuple, Range, and as noted above, Span cannot be const. Also ref structs, which makes slightly more sense. That's probably only because you'd be able to get a reference to a ref struct on the heap, something which shouldn't exist, by using reflection.

Deceptive collection type names

IReadOnly{Collection,Dictionary,List,Set} are for getting read-only views of the collections that implement them. They do not mean the collection is immutable (there are separate classes for that). The same goes for ReadOnlySpan.

Kotlin got this right by calling its interfaces e.g. List/MutableList instead of IReadOnlyList/IList. (And it also fixed the inheritance hierarchy.)

Featureset is determined by language level AND target

see feature matrix page

Guarded default in switch statements

You can only have 1 default branch, but case _ when ...: doesn't work. However, case var _ when ...: does.

MSBuild Condition placement

On (older versions of?) VS, Condition is ignored if placed on a property/item. Create a new <PropertyGroup/>/<ItemGroup/>.

MSBuild path properties

Always use $(MSBuildProjectDirectory) rather than $(ProjectDir) (note that the former doesn't include a trailing slash), because when <Import/>ing a .props file, $(ProjectDir) is unset. ($(SolutionDir) is set, but that should be avoided even in the main solution.)

Use $(TargetPath) rather than reconstructing e.g. $(OutputPath)$(MSBuildProjectName).dll.

Newtonsoft.Json footguns

byte[] is intentionally hardcoded to serialise to a base64 string (as opposed to a list, like short[], int[], etc. are). The only workaround is to implement JsonConverter (already done), then either mark the field/prop, or pass this in the serialiser settings. (This behaviour also made it into System.Text.Json.)

If a string literal contains a date, even if it's being deserialised to a string, it will first be deserialised to a date, timezone-corrected, and re-serialised.

NuGet resources are all for old CLI

up-to-date docs on MSDN

dotnet list $PWD/BizHawk.sln package --outdated will list outdated <PackageReference/>s (betas are ignored without --include-prerelease—if there are only betas published, it sees no releases and prints "Not found"). There is no built-in command for updating them automatically.

NUL-terminated strings

.NET will happily include NUL ((char) 0) in a string if you use String..ctor(char[]). WinForms' Label.Text stops reading at the first NUL for measurement/rendering, at least under Mono.

Preprocessor TFM constants and .NET Standard

The table here is good for reference, but mind the note hidden at the bottom:

The NETSTANDARD<x>_<y>_OR_GREATER symbols are only defined for .NET Standard targets, and not for targets that implement .NET Standard [...]

That is, you must use #if !(NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) and not just #if !NETSTANDARD2_1_OR_GREATER.

Signed/Unsigned byte array casting

As Jon Skeet himself explains here, "Even though in C# you can't cast a byte[] to an sbyte[] directly, the CLR allows it". This leads to some weird behaviour, since the compiler is hardcoded to replace only some type checks with consts.

String.GetHashCode stability

The GetHashCode implementation for strings does not reflect the string's contents, and as such, the hash not stable between program instances.

It seems that Guid's implementation is stable across instances, and even across Mono and .NET 6+ implementations. It also gives 0 for Guid.Empty which is nice.

System.Drawing.Color.* rendered

Docs for Color don't include any pictures, so here's a nice chart.

System.Drawing.SystemIcons rendered

Docs for SystemIcons don't include any pictures, so here they are (Win10, Mono 6.12.x):

SystemIcons_Win10 SystemIcons_Mono

Notice also the default window icon (Form.Icon): on Windows, it's a distinct icon; on Mono (not shown in the screenshot), it resembles SystemIcon.Application. From 2.9, EmuHawk overrides the default to the logo.

[ThreadStatic] field initialisation

Per docs (simpler), static fields initialisation is moved to the static constructor in IL, which runs on at most 1 thread, so a [ThreadStatic] field will be default on all other threads if initialised in the usual way.

Incorrect usage in BizHawk should be flagged with CA2019, but apparently it's not working in CI.

Type casting

There are two types of casts in C#: the C-style (T) o throws if the object is not of the desired type, whereas o as T evaluates to null if it's not of the desired type. There's no '?' in this null-producing operator (this is probably only confusing if you use Kotlin).

If an object being the wrong type is exceptional—the method can't handle it gracefully—then throw a type cast exception straight away. Having it reported as an NRE when there's no null in sight just frustrates debugging efforts.

Type constraints (where clauses)

class in where clauses does not mean "not abstract", it means "reference type". Similarly, struct means "value type". There's a lot of complexity re: nullability, so check the docs if you're writing a generic method.

TODO euler diagram

WinForms Control.ResumeLayout footgun

// works
groupBox.ResumeLayout(performLayout: false);
groupBox.PerformLayout();
// breaks subtly
groupBox.ResumeLayout(performLayout: true);