【问题标题】:Make using statement usable for multiple disposable objects使 using 语句可用于多个一次性对象
【发布时间】:2019-05-26 01:20:08
【问题描述】:

我在一个文件夹中有一堆文本文件,它们都应该有相同的标题。换句话说,所有文件的前 100 行应该是相同的。所以我写了一个函数来检查这个条件:

private static bool CheckHeaders(string folderPath, int headersCount)
{
    var enumerators = Directory.EnumerateFiles(folderPath)
        .Select(f => File.ReadLines(f).GetEnumerator())
        .ToArray();
    //using (enumerators)
    //{
        for (int i = 0; i < headersCount; i++)
        {
            foreach (var e in enumerators)
            {
                if (!e.MoveNext()) return false;
            }
            var values = enumerators.Select(e => e.Current);
            if (values.Distinct().Count() > 1) return false;
        }
        return true;
    //}
}

我使用枚举器的原因是内存效率。我不是将所有文件内容加载到内存中,而是逐行同时枚举文件,直到发现不匹配,或者检查了所有标题。

注释的代码行很明显我的问题。我想使用using 块来安全地处理所有枚举器,但不幸的是using (enumerators) 无法编译。显然using 只能处理一个一次性对象。我知道我可以手动处理枚举数,方法是将整个事物包装在 try-finally 块中,并最终在内部循环中运行处理逻辑,但这似乎很尴尬。在这种情况下,我是否可以采用任何机制使 using 语句成为可行的选择?


更新

我刚刚意识到我的函数有一个严重的缺陷。枚举器的构造并不稳健。锁定的文件可能会导致异常,而一些枚举器已经创建。这些枚举器将不会被释放。这是我想要解决的问题。我正在考虑这样的事情:

var enumerators = Directory.EnumerateFiles(folderPath)
    .ToDisposables(f => File.ReadLines(f).GetEnumerator());

扩展方法ToDisposables 应确保在发生异常时不会留下任何一次性物品。

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 锁定的文件会导致异常”中的“locked”是什么意思?
  • @Alex 我的意思是锁定阅读。例如,当我尝试从我的应用程序打开它时,已经从另一个应用程序打开带有标志 FileShare.None 的文件将导致异常。
  • 我明白了。 var enumerators = Directory.EnumerateFiles(folderPath).Select(f =&gt; { try { return File.ReadLines(f).GetEnumerator(); } catch { return null; } }).Where(f =&gt; f != null).ToArray(); 帮助我以这种方式锁定文件。
  • @Alex 在这种情况下,您将有一个被吞没的异常和一个空枚举器。在我的情况下,我想被告知一个文件被锁定,以便我可以解锁它并再次调用我的函数。对某些文件的标题进行部分检查对我没有用。

标签: c# .net dispose enumeration


【解决方案1】:

您可以在 enumerators 上创建一次性包装器:

class DisposableEnumerable : IDisposable
{
    private IEnumerable<IDisposable> items;

    public event UnhandledExceptionEventHandler DisposalFailed;

    public DisposableEnumerable(IEnumerable<IDisposable> items) => this.items = items;

    public void Dispose()
    {
        foreach (var item in items)
        {
            try
            {
                item.Dispose();
            }
            catch (Exception e)
            {
                var tmp = DisposalFailed;
                tmp?.Invoke(this, new UnhandledExceptionEventArgs(e, false));
            }
        }
    }
}

并以对代码影响最小的方式使用它:

private static bool CheckHeaders(string folderPath, int headersCount)
{
    var enumerators = Directory.EnumerateFiles(folderPath)
        .Select(f => File.ReadLines(f).GetEnumerator())
        .ToArray();

    using (var disposable = new DisposableEnumerable(enumerators))
    {
        for (int i = 0; i < headersCount; i++)
        {
            foreach (var e in enumerators)
            {
                if (!e.MoveNext()) return false;
            }
            var values = enumerators.Select(e => e.Current);
            if (values.Distinct().Count() > 1) return false;
        }
        return true;
    }
}

问题是你必须一个一个地单独处理这些对象。但这取决于您在哪里封装该逻辑。而且我建议的代码没有手册try-finally,)

【讨论】:

  • 我喜欢你的解决方案!我猜using (new DisposableEnumerable(enumerators)) 应该也可以。
  • @Theodor Zoulias,你是完全正确的。只要您不必引用 using 块中的变量,您就可以像这样保持匿名。
  • @Alex,我正在模拟System.IO.IOException: 'The process cannot access the file because it is being used by another process. 我在Dispose 方法中设置了一个断点,但我无法进入它。这是正常行为吗?如果是,我如何确保这些对象已被处置?
  • @Dmitry Stepanov,你能给我更多的上下文吗? '我在Dispose 方法内设置了一个断点'。哪条线?
  • @Alex,我尝试在 Dispose 方法的所有行上进行设置。我被另一个应用程序“锁定”了文件,并且在var enumerators = Directory.EnumerateFiles(folderPath).Select(f =&gt; File.ReadLines(f).GetEnumerator()).ToArray(); 线上抛出了异常,所以,这就是为什么我无法理解您的解决方案是如何工作的。
【解决方案2】:

到问题的第二部分。如果我猜对了,这就足够了:

static class DisposableHelper
{
    public static IEnumerable<TResult> ToDisposable<TSource, TResult>(this IEnumerable<TSource> source,
        Func<TSource, TResult> selector) where TResult : IDisposable
    {
        var exceptions = new List<Exception>();
        var result = new List<TResult>();
        foreach (var i in source)
        {
            try { result.Add(selector(i)); }
            catch (Exception e) { exceptions.Add(e); }
        }

        if (exceptions.Count == 0)
            return result;

        foreach (var i in result)
        {
            try { i.Dispose(); }
            catch (Exception e) { exceptions.Add(e); }
        }

        throw new AggregateException(exceptions);
    }
}

用法:

private static bool CheckHeaders(string folderPath, int headersCount)
{
    var enumerators = Directory.EnumerateFiles(folderPath)
                               .ToDisposable(f => File.ReadLines(f).GetEnumerator())
                               .ToArray();

    using (new DisposableEnumerable(enumerators))
    {
        for (int i = 0; i < headersCount; i++)
        {
            foreach (var e in enumerators)
            {
                if (!e.MoveNext()) return false;
            }
            var values = enumerators.Select(e => e.Current);
            if (values.Distinct().Count() > 1) return false;
        }
        return true;
    }
}

try
{
    CheckHeaders(folderPath, headersCount);
}
catch(AggregateException e)
{
    // Prompt to fix errors and try again
}

【讨论】:

  • 现在你已经涵盖了我的问题的所有情况,但与你的first answer相反,这不是一般的。我对一个通用解决方案感兴趣,该解决方案可以应用于必须安全创建并最终处置一次性物品清单的所有情况。到目前为止,您的first answer 是我成为被接受的主要候选人。 ?
  • @Theodor Zoulias,我改进了解决方案,使其具有通用性。
  • 我们赢了!我接受了你的第一个答案,因为它是独立的。这不是因为它不包含DisposableEnumerable的定义。
  • @Theodor Zoulias,太好了,谢谢!我很高兴我的解决方案很有帮助。
  • @Theodor Zoulias,是的,投票的情况让我很难过。这个问题很有趣也很有用。在我看来,这是一场战斗。赞成票(包括我)与反对票的比例为 4:7。没那么糟糕。好的一面是 +6 声望。
【解决方案3】:

我将建议一种方法,该方法使用对Zip 的递归调用来允许并行枚举普通IEnumerable&lt;string&gt; 而无需诉诸使用IEnumerator&lt;string&gt;

bool Zipper(IEnumerable<IEnumerable<string>> sources, int take)
{
    IEnumerable<string> ZipperImpl(IEnumerable<IEnumerable<string>> ss)
        => (!ss.Skip(1).Any())
            ? ss.First().Take(take)
            : ss.First().Take(take).Zip(
                ZipperImpl(ss.Skip(1)),
                (x, y) => (x == null || y == null || x != y) ? null : x);

    var matching_lines = ZipperImpl(sources).TakeWhile(x => x != null).ToArray();
    return matching_lines.Length == take;
}

现在建立你的enumerables

IEnumerable<string>[] enumerables =
    Directory
        .EnumerateFiles(folderPath)
        .Select(f => File.ReadLines(f))
        .ToArray();

现在调用很简单:

bool headers_match = Zipper(enumerables, 100);

以下是针对三个超过 4 行的文件运行此代码的跟踪:

本·彼得林 5:28 PM ACST 本·彼得林 5:28 PM ACST 本·彼得林 5:28 PM ACST 在 2019-05-23 的电话中,James 提到他希望能够通过管理员编辑当前的运费规则(例如在 shipping_rules.xml 中)。 在 2019-05-23 的电话中,James 提到他希望能够通过管理员编辑当前的运费规则(例如在 shipping_rules.xml 中)。 在 2019-05-23 的电话中,James 提到他希望能够通过管理员编辑当前的运费规则(例如在 shipping_rules.xml 中)。 他还提到他希望能够为给定的时间窗口设置不同的运费规则,例如1 月 1 日至 1 月 30 日。 他还提到他希望能够为给定的时间窗口设置不同的运费规则,例如1 月 1 日至 1 月 30 日。 他还提到他希望能够为给定的时间窗口设置不同的运费规则,例如1 月 1 日至 1 月 30 日。 在选择要使用的适当模块时,应考虑这些故事情节。 在选择要使用的适当模块时应考虑这些故事。X 在选择要使用的适当模块时,应考虑这些故事情节。

请注意,枚举在第二个文件的第 4 行遇到不匹配的标头时停止。然后所有枚举都停止了。

【讨论】:

  • 赞成为我的特定问题提供巧妙的替代解决方案,尽管它不能解决由 using 语句的限制引起的主题问题。例如,如果我想将行同时附加到所有文件,我需要一个打开的 StreamWriter 来为每个必须在最后处理的文件,而 LINQ 在这种情况下无法帮助我(因为 LINQ 用于读取,不写)。
  • 我进行了一些性能测试,并观察到递归的指数开销。对于 500 个文件,我的机器上的开销约为 2 秒。对于 1000 个文件,它变为 15 秒。对于 2000 个文件,它变成 120 秒,等等。
  • @TheodorZoulias - 我没有进行任何性能测试。我通常会尽量防止复合Skip 电话。尝试用ZipperImpl(ss.Skip(1).ToArray()) 替换ZipperImpl(ss.Skip(1)),看看是否会改变性能。
  • @TheodorZoulias - 另请记住,一次读取 2000 个文件一行可能会发生严重的磁盘抖动。
  • 我添加了.ToArray(),是的,它好多了。现在 3000 个文件的开销为 2 秒,而且增长更慢。
【解决方案4】:

按照@Alex 的建议创建IDisposable 包装器是正确的。如果其中一些文件被锁定,它只需要一个逻辑来处理已经打开的文件,并且可能需要一些错误状态的逻辑。可能是这样的(错误状态逻辑很简单):

public class HeaderChecker : IDisposable
{
    private readonly string _folderPath;
    private readonly int _headersCount;
    private string _lockedFile;
    private readonly List<IEnumerator<string>> _files = new List<IEnumerator<string>>();

    public HeaderChecker(string folderPath, int headersCount)
    {
        _folderPath = folderPath;
        _headersCount = headersCount;
    }

    public string LockedFile => _lockedFile;

    public bool CheckFiles()
    {
        _lockedFile = null;
        if (!TryOpenFiles())
        {
            return false;
        }
        if (_files.Count == 0)
        {
            return true; // Not sure what to return here.
        }

        for (int i = 0; i < _headersCount; i++)
        {
            if (!_files[0].MoveNext()) return false;
            string currentLine = _files[0].Current;

            for (int fileIndex = 1; fileIndex < _files.Count; fileIndex++)
            {
                if (!_files[fileIndex].MoveNext()) return false;
                if (_files[fileIndex].Current != currentLine) return false;
            }
        }
        return true;
    }

    private bool TryOpenFiles()
    {
        bool result = true;
        foreach (string file in Directory.EnumerateFiles(_folderPath))
        {
            try
            {
                _files.Add(File.ReadLines(file).GetEnumerator());
            }
            catch
            {
                _lockedFile = file;
                result = false;
                break;
            }
        }
        if (!result)
        {
            DisposeCore(); // Close already opened files.
        }
        return result;
    }

    private void DisposeCore()
    {
        foreach (var item in _files)
        {
            try
            {
                item.Dispose();
            }
            catch
            {
            }
        }
        _files.Clear();
    }

    public void Dispose()
    {
        DisposeCore();
    }
}

// Usage
using (var checker = new HeaderChecker(folderPath, headersCount))
{
    if (!checker.CheckFiles())
    {
        if (checker.LockedFile is null)
        {
            // Error while opening files.
        }
        else
        {
            // Headers do not match.
        }
    }
}

在检查线路时,我还删除了 .Select().Distinct()。第一个只是迭代 enumerators 数组 - 与上面的 foreach 相同,因此您将枚举此数组两次。然后创建一个新的行列表,.Distinct() 对其进行枚举。

【讨论】:

  • 谢谢@StanoPeťko!我赞成您的回答是对我的具体问题的完整解决方案。它确保在所有情况下都处理我的所有枚举器。不过,我不能将其标记为已接受的答案,因为它缺乏通用性。我期待有一种解决方案可以应用于必须安全创建并最终处置一次性物品清单的所有情况。
  • 好吧。包装类需要这些IDisposable 对象中的一些creator 作为输入,以便它可以创建它们并处理错误。它需要一些其他操作来处理创建的项目。但是,如果您想要通用的话,这些项目可以是任何东西。所以处理动作必须知道它正在使用什么类型。进程本身可能想要返回任何东西,而不仅仅是 bool。我相信所有这些都是可能的,但是这样的包装类应该是通用的(可能不止一种通用类型),并且使用它会非常复杂。或者您期望什么样的普遍性?
  • 使包装器通用听起来是个好主意!我同意把它弄得太复杂会破坏这个练习的目的。 using 语句的存在应该是为了简化事情,而不是让事情变得更复杂!
  • 理想情况下,我希望有一个扩展方法 ToDisposables 可以在 LINQ 查询中代替 ToArrayToList,然后能够使用此结果直接在using 语句中的方法:using (enumerators) { ... }
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多