是什么导致 C# 中的结构重置其值?

What causes a struct in C# to reset its values?

我有一个用 C# 编写的基于 ASP.NET Core 3.1 的项目。我有以下 struct 对象,我正尝试使用中间件调用它。

internal struct StaticAsyncFileContext
{
    private const int StreamCopyBufferSize = 64 * 1024;
    private readonly HttpContext _context;
    private readonly StaticAsyncFileOptions _options;
    private readonly HttpRequest _request;
    private readonly HttpResponse _response;
    private readonly ILogger _logger;
    private readonly IAsyncFileProvider _fileProvider;
    private readonly IContentTypeProvider _contentTypeProvider;
    private string _method;
    private bool _isGet;
    private PathString _subPath;
    private string _contentType;
    private IFileInfo _fileInfo;

    private long _length;
    private DateTimeOffset _lastModified;
    private EntityTagHeaderValue _etag;

    private RequestHeaders _requestHeaders;
    private ResponseHeaders _responseHeaders;

    private PreconditionState _ifMatchState;
    private PreconditionState _ifNoneMatchState;
    private PreconditionState _ifModifiedSinceState;
    private PreconditionState _ifUnmodifiedSinceState;

    private RangeItemHeaderValue _range;

    public IFileInfo GetFileInfo()
    {
        return _fileInfo;
    }

    public StaticAsyncFileContext(HttpContext context, StaticAsyncFileOptions options, PathString matchUrl, IAsyncFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
    {
        _context = context;
        _options = options;
        _request = context.Request;
        _response = context.Response;
        _requestHeaders = _request.GetTypedHeaders();
        _responseHeaders = _response.GetTypedHeaders();
        _fileProvider = fileProvider ?? throw new ArgumentNullException($"{nameof(fileProvider)} cannot be null.");
        _contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException($"{nameof(contentTypeProvider)} cannot be null.");
        _subPath = PathString.Empty;
        _method = null;
        _contentType = null;
        _fileInfo = null;
        _length = 0;
        _etag = null;
         _range = null;

        _isGet = false;
        IsHeadMethod = false;
        IsRangeRequest = false;

        _lastModified = new DateTimeOffset();
        _ifMatchState = PreconditionState.Unspecified;
        _ifNoneMatchState = PreconditionState.Unspecified;
        _ifModifiedSinceState = PreconditionState.Unspecified;
        _ifUnmodifiedSinceState = PreconditionState.Unspecified;           
    }

    internal enum PreconditionState
    {
        Unspecified,
        NotModified,
        ShouldProcess,
        PreconditionFailed
    }

    public bool IsHeadMethod { get; private set; }

    public bool IsRangeRequest { get; private set; }

    public string SubPath
    {
        get { return _subPath.Value; }
    }

    public async Task<bool> LookupFileInfoAsync()
    {
        _fileInfo = await _fileProvider.GetFileInfoAsync(_subPath.Value);
        if (_fileInfo.Exists)
        {
            _length = _fileInfo.Length;

            DateTimeOffset last = _fileInfo.LastModified;
            // Truncate to the second.
            _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();

            long etagHash = _lastModified.ToFileTime() ^ _length;
            _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
        }
        return _fileInfo.Exists;
    }

    public void ApplyResponseHeaders(int statusCode)
    {
        // Here _fileInfo should not be null since it was set by LookupFileInfoAsync()

        _response.StatusCode = statusCode;
        if (statusCode < 400)
        {
            // these headers are returned for 200, 206, and 304
            // they are not returned for 412 and 416
            if (!string.IsNullOrEmpty(_contentType))
            {
                _response.ContentType = _contentType;
            }
            _responseHeaders.LastModified = _lastModified;
            _responseHeaders.ETag = _etag;
            _responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
        }
        if (statusCode == Constants.Status200Ok)
        {
            // this header is only returned here for 200
            // it already set to the returned range for 206
            // it is not returned for 304, 412, and 416
            _response.ContentLength = _length;
        }

        _options.OnPrepareResponse(new StaticFileResponseContext(_context, _fileInfo));
    }

    public Task SendStatusAsync(int statusCode)
    {
        ApplyResponseHeaders(statusCode);

        _logger.LogHandled(statusCode, SubPath);
        return Task.CompletedTask;
    }

    public PreconditionState GetPreconditionState()
    {
        return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);
    }

    public async Task SendAsync()
    {
        ApplyResponseHeaders(Constants.Status200Ok);
        string physicalPath = _fileInfo.PhysicalPath;
        var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
        if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
        {
            // We don't need to directly cancel this, if the client disconnects it will fail silently.
            await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
            return;
        }

        try
        {
            using (var readStream = _fileInfo.CreateReadStream())
            {
                // Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
                await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
            }
        }
        catch (OperationCanceledException ex)
        {
            _logger.LogWriteCancelled(ex);
            // Don't throw this exception, it's most likely caused by the client disconnecting.
            // However, if it was cancelled for any other reason we need to prevent empty responses.
            _context.Abort();
        }
    }
}

在上面的struct中,当方法LookupFileInfoAsync()被调用时,假设设置了_fileInfo_length_lastModified属性的值。如果在 LookupFileInfoAsync() 之后调用方法 GetPreconditionState()GetFileInfo(),如果 _fileInfo.Exists return 为真,_fileInfo 的值不应为 null。

下面是我的中间件如何调用这个 struct

public async Task Invoke(HttpContext context)
{
    var fileContext = new StaticAsyncFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);

    if (!await fileContext.LookupFileInfoAsync())
    {
        _logger.LogFileNotFound(fileContext.SubPath);
    } else {

        // This is an unesassary line, it is only added to ensure that FileInfo was set
        IFileInfo fileInfo = fileContext.GetFileInfo();

        switch (fileContext.GetPreconditionState())
        {
            case StaticAsyncFileContext.PreconditionState.Unspecified:
            case StaticAsyncFileContext.PreconditionState.ShouldProcess:
                if (fileContext.IsHeadMethod)
                {
                    await fileContext.SendStatusAsync(Constants.Status200Ok);
                    return;
                }

                try
                {
                    await fileContext.SendAsync();
                    _logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
                    return;
                }
                catch (FileNotFoundException)
                {
                    context.Response.Clear();
                }
                break;
            //... stripped out for simplicity

            default:
                var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
                Debug.Fail(exception.ToString());
                throw exception;
        }
    }

    await _next(context);
}

如您所见,首先调用 LookupFileInfoAsync() 并确保 _infoFile 属性 不为空,因为它 return 为真。但就我而言,在调用 LookupFileInfoAsync() return null 之后调用 fileContext.GetFileInfo()。在单步执行我的代码时,我可以不同地看到 _fileInfo 设置正确。但是在下一个方法调用中,_fileInfo 的值将以某种方式重置为 null。如果您查看 Microsoft 的 StaticFileContext.cs 文件,您会注意到代码非常相似,只是在我的例子中 LookupFileInfo() 被异步调用。

问题

什么可能导致 _fileInfo 属性 在调用 LookupFileInfoAsync() 后重置为 null?

首先,如果结构不可变是理想的。这条规则很可能会解决您的问题。

其次structs值类型,这意味着你很多时候都在复制它们(不是引用给他们)。

就像任何其他值类型一样,如果您复制它们,更改将独立于其他副本

Structs (C# Programming Guide)

Structs are copied on assignment. When a struct is assigned to a new variable, all the data is copied, and any modification to the new copy does not change the data for the original copy. This is important to remember when working with collections of value types such as Dictionary<string, myStruct>.