对象状态与操作合法性异常【C# Exception 详解系列(二)】 – 橙子家

对象状态与操作合法性异常【C# Exception 详解系列(二)】 – 橙子家


This article mainly introduces object status and operation legality exceptions, which mainly include four types: InvalidOperationException, ObjectDisposedException, NotSupportedException, NotImplementedException, PlatformNotSupportedException, and TimeoutException.

〇.Foreword

The previous article has listed all the common abnormal types. This article continues to introduce the abnormal characteristics of each category.

This article mainly introduces object status and operation legality anomalies, mainly including the following types:InvalidOperationException、ObjectDisposedException、NotSupportedException、NotImplementedException、PlatformNotSupportedException、TimeoutException。

Click the link to view this series of articles:C# Exception detailed explanation series.

1. Overview of parameter verification exceptions

The common point of such exceptions is that the caller’s code syntax is correct and the parameters are correct, but the “timing” or “environment” is wrong.Mainly deal with the following three issues——Is the subject still alive? Is the object now in a state that allows this operation? Do you have this ability? The essence of selection is to distinguish between these three.

Exception type core meaning permanent inheritance relationship Does retrying help? preferred alternative
InvalidOperationException Wrong status/timing No (reordering may succeed) Exception It depends on the situation Disassemble the interface and state machine
ObjectDisposedException released yes InvalidOperationException useless life cycle governance
NotSupportedException There is no such ability in design yes Exception useless Change to narrow interface/CanXxx detection
NotImplementedException Not written yet No (will happen in the future) Exception useless Completion implementation
PlatformNotSupportedException The current platform does not have this capability Yes (just change the machine) NotSupportedException useless Runtime platform branch downgrade
TimeoutException Waiting for timeout No (retry may succeed) Exception Caution (idempotence) CancellationToken

The three pairs that are most likely to be chosen incorrectly:

  1. ArgumentException vs InvalidOperationException: The former means “what you passed on is wrong”, and the latter means “what you passed on is right, but it can’t be done now”. Judgment criteria: Can the same parameters be successful at another time? Can → InvalidOperation.
  2. NotSupportedException vs NotImplementedException: Design issues vs. progress issues. If you choose the opposite, the caller will make wrong decisions (such as bypassing your implementation, or waiting for you to release the version).
  3. TimeoutException vs OperationCanceledException: Synchronous/protocol timeout vs asynchronous cancellation. The former is in System.Runtimethe latter in System.Threadingeven the namespace is different.

Key memory points: ObjectDisposedException inherits from InvalidOperationException; PlatformNotSupportedException inherits from NotSupportedException. Therefore, when catching the base class, the subclass will be captured “incidentally”, and if written in reverse order, more precise information will be swallowed up.

Here are a few general disciplines to note.

  1. Bring context: Write clearly “current status/desired status/which object/which platform/how many milliseconds to time out” in the message. These anomalies usually mean process defects online, and just looking at the type is not enough to locate them.
  2. XML document markup:/// so that the caller can see the preconditions in the IDE.
  3. If it can be expressed using Boolean/enumeration/interface, do not use exceptions.:TryOpen()、CanWrite、IReadOnlyList All are better than “set and then catch”.
  4. Don’t use them for control flow on hot paths:foreach Catch NotSupportedException Judging whether it can be written or not, the performance will be ugly.
  5. The catch order is from fine to coarse:First ObjectDisposedException Again InvalidOperationException;First PlatformNotSupportedException Again NotSupportedException。
  6. Custom exceptions should be restrained: These six types have covered 99% of status errors.It’s only worth deriving your own exception if “the caller needs to catch it separately and do special recovery”(And remember to set HResult and Serializable).

2. Introduction and simple examples of each exception

2.1 InvalidOperationException – “Not now”

Semantics:The object is in aLegal but not suitable for this operationstatus. It is not a permanent negation (change the timing and it will succeed), nor is it a parameter error. This is the most commonly used and easily abused category in this group.

Typical BCL cases:

  • SqlConnection not yet Open() Just execute ExecuteReader
  • IEnumerator.MoveNext() When it is found that the collection has been modified (detected internally by the version counter), throw InvalidOperationException:“Collection was modified; enumeration operation may not execute.”
  • List.Remove() on readonly wrapper
  • LINQ Single() / First() on empty sequence;Enumerable.Cast() Transformation failed
  • exist foreach Modify the collection in progress;DataTable Written during BeginLoadData
// 一个简单的示例:
public class OrderProcessor
{
    private enum State { Created, Started, Completed }
    private State _state = State.Created;
    private readonly List _items = new();
    
    public void Start()
    {
        if (_state != State.Created)
            throw new InvalidOperationException(
                $"Order cannot be started in {_state} state.");
        _state = State.Started;
    }
    public void AddItem(string item)
    {
        if (_state == State.Completed)
            throw new InvalidOperationException("Order already completed.");
        _items.Add(item);
    }
    public IEnumerable GetItems()
    {
        foreach (var it in _items)   // 调用方若在此期间修改 _items,MoveNext 会抛 InvalidOperationException
            yield return it;
    }
}

Applicable scenarios:

  • There is clearstate machineObjects (connections, transactions, orders, tasks, flows)
  • The resource object that needs to call Init/Open/BeginXxx first
  • Enumerators, iterators, delayed execution (yield / LINQ) legality check
  • The race condition of “having been completed by others first” under multi-threading (cooperating with Interlocked judgment)

Common misuses:

  1. Wrong parameter wrapping into it:index Crossing the line should be thrown ArgumentOutOfRangeExceptionnot InvalidOperation.
  2. Use it to cover up design flaws: If a method “often cannot be called”, it means that the interface is too fat and should be split into ICanStart / IStartable Wait for small interfaces to make unsupported states invisible at compile time.
  3. There is no status information in the message. A good message should state “what is the current status and what is the expected status” to facilitate online troubleshooting.

2.2 ObjectDisposedException – “Already dead”

Semantics:The object’s life cycle has endedany member (except again Dispose ) should not be called. it is InvalidOperationException A subclass of , but with narrower and more diagnostic semantics.

Areas that require special attention:

  • Dispose Must be idempotentthe second call should not throw an exception; what is thrown is “other members are called after Dispose”.
  • Standard practice is to do a disposed check at the beginning of each public member; field _disposed It is recommended to use bool(simple scenario) or combined GC.SuppressFinalize。
  • .NET 7+ provided ObjectDisposedException.ThrowIf(bool condition, object instance)it can be done in one line and the message format is unified.
  • Don’t throw from a finalizer; don’t expect managed objects to be available during finalization.
  • Note on asynchronous scenarios:DisposeAsync There is still await running later, which is a typical “released but still used”.
public sealed class LogWriter : IDisposable
{
    private readonly StreamWriter _writer;
    private bool _disposed;

    public LogWriter(string path) => _writer = new StreamWriter(path, append: true);

    public void Write(string message)
    {
        // .NET 7+ 推荐写法:ObjectDisposedException.ThrowIf(_disposed, this);
        if (_disposed) throw new ObjectDisposedException(nameof(LogWriter));

        _writer.WriteLine($"{DateTime.Now:O} {message}");
    }
    public void Flush()
    {
        if (_disposed) throw new ObjectDisposedException(nameof(LogWriter));
        _writer.Flush();
    }
    public void Dispose()
    {
        if (_disposed) return;     // 幂等
        _writer.Dispose();         // 释放资源
        _disposed = true;          // 标志置位必须在释放之后(避免释放失败时误判为已释放)
    }
}

// 调用方
var log = new LogWriter("app.log");
log.Dispose();
log.Write("oops");   // ObjectDisposedException: Cannot access a disposed object. Object name: 'LogWriter'.

Applicable scenarios:

  • All implementations IDisposable / IAsyncDisposable type (stream, connection, handle wrapper, cache file, timer)
  • Return the pooled object before using it again
  • HttpClient is reused after Dispose (a very common online bug)

Trade-off with InvalidOperationException.If the type is not implemented IDisposablejust use InvalidOperationException。ObjectDisposedException is the exclusive signal of Dispose modeit can directly point to life cycle management issues when monitoring alarms and log retrieval, so it is worth using alone.

2.3 NotSupportedException – “Not by design”

Semantics:The member exists in the interface/base class contract, butThe current implementation is by design unable to provide. This is not a temporary state, nor will it be retried ten thousand times.

Typical BCL cases:

  • Stream read only CanWrite == falsecall Write → NotSupportedException
  • Array / Fixed size list implementation IList.Add → NotSupportedException
  • ReadOnlyCollection.Add
  • Encoding.GetEncoding("unsupported") Some scenarios (more ArgumentException)
  • ICollection.IsReadOnly == true How to modify the time
public class ReadOnlyStream : Stream
{
    private readonly Stream _inner;
    public ReadOnlyStream(Stream inner) => _inner = inner;

    public override bool CanRead => true;
    public override bool CanWrite => false;   // 关键:先提供探测属性
    public override bool CanSeek => _inner.CanSeek;

    public override long Length => _inner.Length;
    public override long Position { get => _inner.Position; set => throw new NotSupportedException(); }

    public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);

    public override void Write(byte[] buffer, int offset, int count) =>
        throw new NotSupportedException("This stream is read-only.");

    public override void Flush() { }
    public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
    public override void SetLength(long value) => throw new NotSupportedException();
}

Applicable scenarios:

  • Implementing a “wide interface” (IList、IDbCommand、Stream、ICollection) is a subset of capabilities
  • Missing low-level capabilities in adapter/decorator pattern
  • Some operations in abstract base classes are meaningless to some subclasses

Best practice: Don’t let the caller rely on exception programming if you can detect it.The standard mode of BCL is CanXxx attribute + throw exceptionDouble insurance:

// 推荐:先问,再做
if (stream.CanWrite)
    stream.Write(buf, 0, len);

// 不推荐:做了再说,靠异常当分支
try { stream.Write(buf, 0, len); }
catch (NotSupportedException) { /* 哦,原来不能写 */ }

The document should clearly state “which members will be thrown” and mark it in the XML comments. 。

Reflection on the design level.If you find yourself often Add/Remove throw NotSupportedExceptiongive priority toChange interface:use IReadOnlyList replace IList,use IEnumerable replace ICollection. Letting compile-time and IDE prompts prevent illegal calls is an order of magnitude better than throwing exceptions at runtime.

2.4 NotImplementedException – “Not done yet”

Semantics:code pathDoes not exist yetbelongs to the development period. The difference between it and “not supported” is the intent:There will be in the future, but not now。

HResult is 0x80004001 (E_NOTIMPL),and NotSupportedException yes 0x80131515——It can be separated at a glance in the debugger.

public class PaymentService : IPaymentService
{
    public Task PayAsync(decimal amount) => throw new NotImplementedException("微信支付尚未接入,预计 Q4 完成");

    // IDE 自动生成接口实现时默认就填这个,方便你逐个实现
    public Task RefundAsync(string id) => throw new NotImplementedException();
}

Applicable scenarios:

  • After IDE generates interface/overridden methodPlaceholder(This is its main and most legitimate use)
  • When delivering large functions in iterations, let the project compile and pass first
  • Rare case: the base class provides optional hooks and subclasses do not need to override them (this is more recommended virtual Empty implementation or abstract class)

Where not to use it:

  • should never appear in the normal running path of production code. It must be cleared before going online, otherwise the user will see a NotSupportedException More difficult to understand errors.
  • Don’t use it to mean “this subclass will never have this ability” – that’s NotSupportedException。
  • Don’t use it to express “the parameter combination has not been processed yet” – that is ArgumentException or InvalidOperationException。

Memory tips:NotImplemented = timing issue; NotSupportedException = design issue。

2.5 PlatformNotSupportedException – “This machine cannot run”

Semantics: The API is compiled in (usually a shared code base/multi-targeting), but the underlying capabilities are not provided by the current OS or runtime. it is NotSupportedException subclass, so when catching the parent class, it will be captured together – pay attention to the catch order.

Typical BCL cases:

  • Microsoft.Win32.Registry On non-Windows platforms
  • ProcessStartInfo.Verb = "runas"(Elevated privileges) on non-Windows
  • WindowsIdentity / WindowsPrincipal In Linux
  • Console.Beep() In some Unix terminals
  • Rfc2898DeriveBytes Certain hardware acceleration paths forVector Hardware intrinsics are not supported on CPUs
  • Call desktop-specific API in MAUI / Blazor Hybrid
using System.Runtime.InteropServices;

public static class OsHelper
{
    public static void SetRegistryValue(string key, string value)
    {
        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            throw new PlatformNotSupportedException(
                "Registry is only available on Windows. Use a config file on Linux/macOS.");

        Microsoft.Win32.Registry.CurrentUser.CreateSubKey(key).SetValue("App", value);
    }

    // 更好的做法:主动降级而不是抛
    public static string GetConfigPath() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
            ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "app.cfg")
            : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".app.cfg");
}

Applicable scenarios:

  • Shared libraries for multi-target frameworks (netstandard/net8.0+windows)
  • Preserve old Windows-specific API stubs during containerization/cross-platform migration
  • conditional compilation (#if WINDOWS) other than runtime branches

Best practices:

  1. priority RuntimeInformation.IsOSPlatform / OperatingSystem.IsWindows() Make an active branchgracefully degrade or return to alternatives, and don’t let users run into the depths before hitting exceptions.
  2. The message states “Which platform supports it and what alternatives are available for the current platform”.
  3. Used for unit testing [Fact(Skip = ...)] or OperatingSystem Guard to prevent CI from running Windows use cases on Linux.

2.6 TimeoutException – “Waiting too long”

Semantics:The operation was not completed within the agreed time.

Notice:it meansWait for strategy to be exhausteddoes not mean that the other party failed, nor does it mean that the operation did not occur.(It may already be executing, and repeated retries may cause problems such as double deductions).

public async Task FetchWithTimeoutAsync(HttpClient client, Uri uri, TimeSpan timeout)
{
    using var cts = new CancellationTokenSource(timeout);
    try
    {
        return await client.GetStringAsync(uri, cts.Token);
    }
    catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested)
    {
        // HttpClient 超时会抛出 TaskCanceledException(派生自 OperationCanceledException)
        throw new TimeoutException($"GET {uri} timed out after {timeout.TotalSeconds}s.");
    }
}

// 同步/协议级超时:串口、WCF、自定义握手
public byte[] ReadExactly(SerialPort port, int count, int millisecondsTimeout)
{
    var buf = new byte[count];
    var end = DateTime.UtcNow.AddMilliseconds(millisecondsTimeout);
    int read = 0;
    while (read < count)
    {
        if (DateTime.UtcNow > end)
            throw new TimeoutException($"Expected {count} bytes, got {read}.");
        port.ReadTimeout = Math.Max(1, (int)(end - DateTime.UtcNow).TotalMilliseconds);
        read += port.Read(buf, read, count - read);
    }
    return buf;
}

Applicable scenarios:

  • Synchronous blocking IO, serial port/socket reading and writing, protocol handshake
  • Distributed lock/semaphore wait timeout (SemaphoreSlim.Wait(timeout) Can be re-thrown when false is returned)
  • Client-side encapsulation of external system calls (putting the underlying TaskCanceledException Uniformly translated into business readable TimeoutException)
  • “Don’t wait if you can’t get it” in cache penetration protection

Important Pitfalls: TPL / Async Cancellation Don’t use it:

  • CancellationToken timeout → throw OperationCanceledException / TaskCanceledException
  • HttpClient timeout → TaskCanceledException
  • Task.Wait/Result timeout → return false or package in AggregateException inside

That is to say:TimeoutException is the language of “protocol/sync-wait”,OperationCanceledException Is the language of “asynchronous cancellation”. Mixing them will make all catch logic invalid. Additionally, capture TimeoutException Before trying again, be sure to ask yourself: Has the upstream already taken effect? (Impotence/compensation mechanism)

3. Compare the above anomalies in a real scene

The following is an imaginary “message sender” with four types of exceptions performing their own duties:

public class SmtpSender : IDisposable
{
    private bool _disposed;
    private bool _connected;
    private readonly int _timeoutMs;

    public SmtpSender(int timeoutMs = 5000) => _timeoutMs = timeoutMs;

    public void Connect()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (_connected) throw new InvalidOperationException("Already connected.");
        // ... 真正连
        _connected = true;
    }

    public void Send(string to, string body)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (!_connected) throw new InvalidOperationException("Send requires Connect() first.");
        // ... 发送,带超时        
        // 组合:调用方超时 + 本类默认超时
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        linkedCts.CancelAfter(_timeoutMs);
        try
        {
            await _client.SendMailAsync(to, body, linkedCts.Token);
        }
        catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
        {
            // 是自己的超时触发的 → 转成业务可读的 TimeoutException
            throw new TimeoutException($"Send timed out after {_timeoutMs}ms.", ex);
        }
        // 调用方主动取消:原样抛出 OperationCanceledException,不要包成 TimeoutException
    }

    // 这个实现只会发文本,附件从设计上就不支持
    public virtual void SendWithAttachment(string to, string body, string path) =>
        throw new NotSupportedException("Attachments are not supported by this sender.");

    // 下个迭代才接的模板功能
    public void SendTemplate(string templateId, object model) =>
        throw new NotImplementedException("Template sending planned for v2.");

    // 只在 Windows 上可用的 SSO 登录
    public void LoginWithCurrentUser()
    {
        if (!OperatingSystem.IsWindows())
            throw new PlatformNotSupportedException("SSPI login requires Windows.");
    }

    public void Dispose()
    {
        if (_disposed) return;
        _connected = false;
        _disposed = true;
    }
}

Each of the five exceptions in the same class belongs to its own place, and the caller can judge “should I reconnect, change the implementation, upgrade the version, or change the operating system” by relying on the type.

4. A small summary

These six exceptions handle the situation of “the parameters are correct, but the call still fails”. The core is to distinguish whether the cause of the failure is that the object is still alive, whether the ability exists, or whether the timing is right.

Can be divided into three groups according to properties:

  1. The only life cycle problem is ObjectDisposedExceptionindicating that the object has been released and cannot be restored, provided that the type implements IDisposable, otherwise InvalidOperationException should be returned;
  2. Capability issues include NotSupportedException (not supported by design), NotImplementedException (not finished yet, should be cleared before going online), and PlatformNotSupportedException (not supported by the current platform). All three are permanent failures, and retrying is meaningless;
  3. Timing and environment issues are InvalidOperationException (the status or sequence is wrong, it may succeed at another time) and TimeoutException (the waiting strategy is exhausted, which does not mean that the operation has not occurred).

When selecting, ask yourself: Is the object released? Is the capability not in the design, not written yet, or is it not supported by the platform? Is the status wrong? Have you waited too long?Each answer corresponds to a different direction of repair: managing the life cycle, narrowing the interface or completing the implementation, correcting the call sequence, and improving the timeout and cancellation strategy.

Three common mistakes need to be noted:The difference between ArgumentException and InvalidOperationException lies in whether the same parameter can be successfully changed at another time; the latter two are design issues and progress issues, and choosing the opposite will mislead the caller; use TimeoutException for synchronous timeout, and OperationCanceledException for asynchronous cancellation. Mixed use will cause the catch logic to fail.In addition, the inheritance relationship determines that the catch order must be from fine to coarse, capturing the subclass first and then the parent class.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *