-
Notifications
You must be signed in to change notification settings - Fork 389
C# and .NET docs supplement
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.
Under .NET 8 (all of this is untested, just based on the docs and .NET issue tracker):
- The largest 1D byte array is
new byte[Array.MaxLength]
,Array.MaxLength
being hardcoded to0x7FFF_FFC7
or just under 2 GiB. (Would probably want to alloc in 1 GiB chunks to have nice code, maybe even smaller to keep the GC happy.) - The largest 1D struct array is
new T[Array.MaxLength]
. So its size is... unbounded since structs don't have a size limit apart from the stack size (and[InlineArray]
's poorly-documented 1 MiB cap). (TODO check there isn't an undocumented cap on struct size) - The largest n-D byte array is
UNK()
(LongLength
is0xUNK
or UNK GiB). - If a call would allocate beyond those limits (or the process' or machine's limits), an
OutOfMemoryException
will be thrown, which can be caught and handled but definitely shouldn't be. - All of that is on the managed heap. The default/global stack is OS-specific and cannot be changed: On Windows, it's 1.5 MiB due to an oversight, and on Linux, it's inherited from the OS, typically 8 MiB.
- The stack size for managed threads can be specified on init, though as with all of this you may have an XY problem and should reconsider.
- If a call would allocate beyond the stack size (or a call chain grows out of control), a
StackOverflowException
will be thrown, which cannot be caught.
Under Mono (x64 unless specified and without gcAllowVeryLargeObjects
, TODO see if Mono respects that):
- The largest 1D byte array is
new byte[int.MaxValue]
(just under 2 GiB), as there is no way to instantiate an array with a larger size (Array.CreateInstance(typeof(T), 0x8000_0000L)
will throwArgumentOutOfRangeException: Arrays larger than 2GB are not supported.
, which I suppose is technically incorrect). - Similarly, the largest 1D struct array is
new T[int.MaxValue]
. So its size is... unbounded since structs on the heap don't have a size limit. - The largest n-D byte array is
new byte[2, 0x7FFF_FFE6]
(LongLength
is0xFFFF_FFCC
or just under 4 GiB). Allocating a single byte more gives an OoME. Multidimensional arrays of other structs appear to also be limited to0xFFFF_FFCC
octets. - If a call would allocate beyond those limits (or the process' or machine's limits), the relevant builtin method will throw an
OutOfMemoryException
, which can be caught and handled but definitely shouldn't be. - All of that is on the managed heap. The default/global stack is UNK MiB large and can be changed externally with
ulimit
(a builtin in most POSIX shells, including BASH), but not with Roslyn.- The stack size for managed threads can be specified on init, though as with all of this you may have an XY problem and should reconsider. The max. is 1 MiB / 2 MiB (32-bit/64-bit hosts), and there is a hack to bypass the limit (untested).
- If a
stackalloc
call would allocate beyond those limits (withstackalloc
; TODO check this), or a call chain grows out of control, aStackOverflowException
will be thrown, which cannot be caught.
Under .NET Framework on Windows (x64 unless specified):
- Untested, but I'm guessing it's probably the same as Mono so long as the
gcAllowVeryLargeObjects
is enabled in the runtime config, going by those docs and this blog post.- Or maybe not; see this SO answer and the comment below it.
- The default/global stack is 1 MiB, specified in a PE header. This can be changed externally with
EDITBIN
, but not with Roslyn nor at runtime in any way.- The stack size for managed threads defaults to 1 MiB / 4 MiB (x86/x64) (TODO surely there's an official source) can be specified on init, though as with all of this you may have an XY problem and should reconsider. The max. is UNK MiB (default AppDomain; 1 MiB if untrusted) and the min. seems to be OS-dependent but was ~256 KiB on Vista and presumably hasn't been changed since.
- If a call would allocate beyond the stack size (or a call chain grows out of control), a
StackOverflowException
will be thrown, which cannot be caught.
- It should be no surprise that all these limits were inherited from native Win32. Thankfully modern .NET is deviating from that where necessary.
The API reference on Microsoft Learn (formerly MSDN) now links to the source in most places, but of course that's for modern .NET.
- For .NET Framework, use the Reference Source (or via GitHub).
- For Mono... try this IDK.
- And for modern .NET where the links are missing, you can search the BCL with the new .NET Source Browser or on GitHub like so.
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.
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.
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 ofIReadOnlyList
/IList
. (And it also fixed the inheritance hierarchy.)
see feature matrix page
You can only have 1 default:
branch (not to be confused with case default:
), but you can't simply add a guard clause: case _ when ...:
doesn't work. However, case var _ when ...:
does.
See this SO answer. tl;dr: It's possible in .NET Core only.
full implementations of extension methods for later
public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span)
where TTo: class
where TFrom: class
{
if (typeof(TTo).IsAssignableFrom(typeof(TFrom))) throw new InvalidCastException();
return UnsafeDowncast<TFrom, TTo>(span);
}
public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span)
where TTo: class
where TFrom: class
{
if (typeof(TTo).IsAssignableFrom(typeof(TFrom))) throw new InvalidCastException();
return UnsafeDowncast<TFrom, TTo>(span);
}
public static Span<TBase> CastUp<TDerived, TBase>(Span<TDerived> span)
where TBase: class
where TDerived: class, TBase
=> UnsafeDowncast<TDerived, TBase>(span);
public static ReadOnlySpan<TBase> CastUp<TDerived, TBase>(ReadOnlySpan<TDerived> span)
where TBase: class
where TDerived: class, TBase
=> UnsafeDowncast<TDerived, TBase>(span);
public static Span<TTo> UnsafeDowncast<TFrom, TTo>(Span<TFrom> span)
where TTo: class
where TFrom: class
=> MemoryMarshal.CreateSpan<TTo>(
ref Unsafe.As<TFrom, TTo>(ref MemoryMarshal.GetReference(span)),
span.Length);
public static ReadOnlySpan<TTo> UnsafeDowncast<TFrom, TTo>(ReadOnlySpan<TFrom> span)
where TTo: class
where TFrom: class
=> MemoryMarshal.CreateReadOnlySpan<TTo>(
ref Unsafe.As<TFrom, TTo>(ref MemoryMarshal.GetReference(span)),
span.Length);
On (older versions of?) VS, Condition
is ignored if placed on a property/item. Create a new <PropertyGroup/>
/<ItemGroup/>
.
Example:
$> dotnet publish --getProperty:IsTargetingNetFramework src/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj
False
$> dotnet publish --getProperty:IsTargetingNetFramework src/BizHawk.WinForms.Controls/BizHawk.WinForms.Controls.csproj
True
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
.
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.
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.
.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.
See dotnet/csharplang#53622/dotnet/csharplang#90939.
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
.
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.
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.
Docs for Color
don't include any pictures, so here's a nice chart.
Docs for SystemIcons
don't include any pictures, so here they are (Win10, Mono 6.12.x):
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.
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.
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.
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
// works
groupBox.ResumeLayout(performLayout: false);
groupBox.PerformLayout();
// breaks subtly
groupBox.ResumeLayout(performLayout: true);