Сделайте использование оператора пригодным для нескольких одноразовых предметов - PullRequest
0 голосов
/ 26 мая 2019

У меня есть куча текстовых файлов в папке, и все они должны иметь одинаковые заголовки.Другими словами, первые 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 должен гарантировать, что в случае исключения одноразовые изделия не останутся нераспределенными.

Ответы [ 4 ]

3 голосов
/ 19 июня 2019

Вы можете создать одноразовую упаковку поверх вашего 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,)

2 голосов
/ 20 июня 2019

Ко второй части вопроса. Если я правильно понимаю, этого должно быть достаточно:

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
}
2 голосов
/ 26 мая 2019

Я собираюсь предложить подход, который использует рекурсивные вызовы Zip, чтобы позволить параллельное перечисление нормального IEnumerable<string> без необходимости прибегать к использованию IEnumerator<string>.

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 строками:

Ben Petering at 5:28 PM ACST 
Ben Petering at 5:28 PM ACST 
Ben Petering at 5:28 PM ACST 
  From a call 2019-05-23, James mentioned he’d like the ability to edit the current shipping price rules (eg in shipping_rules.xml) via the admin. 
  From a call 2019-05-23, James mentioned he’d like the ability to edit the current shipping price rules (eg in shipping_rules.xml) via the admin. 
  From a call 2019-05-23, James mentioned he’d like the ability to edit the current shipping price rules (eg in shipping_rules.xml) via the admin. 
He also mentioned he’d like to be able to set different shipping price rules for a given time window, e.g. Jan 1 to Jan 30.
He also mentioned he’d like to be able to set different shipping price rules for a given time window, e.g. Jan 1 to Jan 30.
He also mentioned he’d like to be able to set different shipping price rules for a given time window, e.g. Jan 1 to Jan 30.
These storyishes should be considered when choosing the appropriate module to use.
These storyishes should be considered when choosing the appropriate module to use.X
These storyishes should be considered when choosing the appropriate module to use.

Обратите внимание, что перечисления прекращаются, когда они сталкиваются с несоответствующим заголовком в 4-й строке второго файла.Затем все перечисления прекратились.

1 голос
/ 20 июня 2019

Создание оболочки IDisposable, как предложено @Alex, является правильным. Нужна просто логика для удаления уже открытых файлов, если некоторые из них заблокированы и, возможно, некоторая логика для состояний ошибок. Может быть что-то вроде этого (логика состояния ошибки очень проста):

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() перечисляет его.

...