Передача одного элемента как IEnumerable <T>


377

Есть ли общий способ передачи одного элемента типа Tв метод, который ожидает IEnumerable<T>параметр? Язык - C #, версия фреймворка 2.0.

В настоящее время я использую вспомогательный метод (это .Net 2.0, поэтому у меня есть целая куча вспомогательных методов приведения / проектирования, похожих на LINQ), но это просто кажется глупым:

public static class IEnumerableExt
{
    // usage: IEnumerableExt.FromSingleItem(someObject);
    public static IEnumerable<T> FromSingleItem<T>(T item)
    {
        yield return item; 
    }
}

Другой способ, конечно, состоит в том, чтобы создать и заполнить List<T>или Arrayпередать и передать его вместо IEnumerable<T>.

[Edit] В качестве метода расширения его можно назвать:

public static class IEnumerableExt
{
    // usage: someObject.SingleItemAsEnumerable();
    public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item)
    {
        yield return item; 
    }
}

Я что-то здесь упускаю?

[Edit2] Мы нашли someObject.Yield()(как предложил @Peter в комментариях ниже) лучшее имя для этого метода расширения, в основном для краткости, поэтому здесь, наряду с комментарием XML, если кто-то захочет его взять:

public static class IEnumerableExt
{
    /// <summary>
    /// Wraps this object instance into an IEnumerable&lt;T&gt;
    /// consisting of a single item.
    /// </summary>
    /// <typeparam name="T"> Type of the object. </typeparam>
    /// <param name="item"> The instance that will be wrapped. </param>
    /// <returns> An IEnumerable&lt;T&gt; consisting of a single item. </returns>
    public static IEnumerable<T> Yield<T>(this T item)
    {
        yield return item;
    }
}

6
Я бы внес небольшую модификацию в тело метода расширения: if (item == null) yield break; теперь вам не разрешено передавать значение null, а также использовать (тривиальный) шаблон нулевого объекта для IEnumerable. ( foreach (var x in xs)справляется с пустым xsпросто отлично). Между прочим, эта функция является монадической единицей для монады списка, которая есть IEnumerable<T>, и, учитывая монаду love-fest в Microsoft, я удивлен, что что-то подобное не входит в рамки в первую очередь.
Мэтт Энрайт

2
Для метода расширения его не следует называть, AsEnumerableпотому что встроенное расширение с таким именем уже существует . (Когда Tреализует IEnumerable, например,. string)
Джон-Эрик

22
Как насчет названия метода Yield? Ничто не сравнится с краткостью.
Филипп Гуин

4
Названия предложений здесь. "SingleItemAsEnumerable" немного многословно. «Выход» описывает реализацию, а не интерфейс - что не очень хорошо. Для лучшего названия я предлагаю «AsSingleton», которые соответствуют точному значению поведения.
Земля Двигатель

4
Я ненавижу left==nullчек здесь. Это нарушает красоту кода и делает его более гибким - что если однажды вам понадобится синглтон с чем-то, что может быть нулевым? Я имею в виду, new T[] { null }не то же самое new T[] {}, и когда-нибудь вам, возможно, придется различать их.
Земля Двигатель

Ответы:


100

Ваш вспомогательный метод - самый чистый способ сделать это, ИМО. Если вы передадите список или массив, то недобросовестный кусок кода может привести к его преобразованию и изменению содержимого, что в некоторых ситуациях приведет к странному поведению. Вы можете использовать коллекцию только для чтения, но это, вероятно, потребует еще большей упаковки. Я думаю, что ваше решение так же аккуратно, как и получается.


хорошо, если список / массив построен ad-hoc, его область действия заканчивается после вызова метода, поэтому это не должно вызывать проблем
Mario F

Если он отправлен в виде массива, как его можно изменить? Я предполагаю, что это может быть приведено к массиву, а затем изменить ссылку на что-то еще, но что хорошего это будет делать? (Я, вероятно, что-то
упускаю,

2
Предположим, вы решили создать один перечислимый для передачи двум разным методам ... затем первый преобразовал его в массив и изменил содержимое. Затем вы передаете его в качестве аргумента другому методу, не подозревая об изменениях. Я не говорю, что это вероятно, просто то, что что-то изменчивое, когда это не нужно, менее аккуратно, чем неизменность натурлы.
Джон Скит

3
Это принятый ответ, и, скорее всего, его прочтут, поэтому я добавлю свою озабоченность здесь. Я попробовал этот метод, но это сломало мой ранее скомпилированный вызов, myDict.Keys.AsEnumerable()где myDictбыл тип Dictionary<CheckBox, Tuple<int, int>>. После того как я переименовал метод расширения в SingleItemAsEnumerable, все начало работать. Невозможно преобразовать IEnumerable <Dictionary <CheckBox, System.Tuple <int, int >>. KeyCollection> 'в' IEnumerable <CheckBox> '. Существует явное преобразование (вам не хватает актеров?) Я могу некоторое время жить с хаком, но могут ли другие силовые хакеры найти лучший способ?
Хэмиш Грубиджан

3
@HamishGrubijan: Звучит так, будто вы хотели просто dictionary.Valuesначать с. В принципе, не ясно, что происходит, но я предлагаю вам начать новый вопрос об этом.
Джон Скит

133

Хорошо, если метод ожидает, что IEnumerableвы должны передать что-то, что является списком, даже если он содержит только один элемент.

прохождение

new[] { item }

в качестве аргумента должно быть достаточно, я думаю,


32
Я думаю, что вы могли бы даже опустить T, так как он будет выведен (если я не очень ошибаюсь здесь: p)
Svish

2
Я думаю, что это не выводится в C # 2.0.
Groo

4
«Язык - это C #, версия платформы 2» Компилятор C # 3 может вывести T, и код будет полностью совместим с .NET 2.
sisve

лучший ответ внизу ?!
Falco Alexander

2
@Svish Я предложил и отредактировал ответ, чтобы сделать именно это - мы сейчас в 2020 году, так что это должен быть «стандартный» imho
OschtärEi

120

В C # 3.0 вы можете использовать класс System.Linq.Enumerable:

// using System.Linq

Enumerable.Repeat(item, 1);

Это создаст новый IEnumerable, который содержит только ваш элемент.


26
Я думаю, что это решение затруднит чтение кода. :( Повторять один элемент довольно
нелогично,

6
Повторите, как только читает хорошо для меня. Гораздо более простое решение, не нужно беспокоиться о добавлении еще одного метода расширения и о том, что пространство имен включено везде, где вы хотите его использовать.
si618

13
Да, я просто использую new[]{ item }себя, я просто подумал, что это интересное решение.
Луксан

7
Это решение - деньги.
ProVega

ИМХО это должен быть принятый ответ. Это легко читать, кратко, и это реализовано в одной строке на BCL, без специального метода расширения.
Райан

35

В C # 3 (я знаю, что вы сказали 2), вы можете написать общий метод расширения, который может сделать синтаксис немного более приемлемым:

static class IEnumerableExtensions
{
    public static IEnumerable<T> ToEnumerable<T>(this T item)
    {
        yield return item;
    }
}

клиентский код тогда item.ToEnumerable().


Спасибо, я знаю об этом (работает в .Net 2.0, если я использую C # 3.0), мне просто интересно, был ли встроенный механизм для этого.
Groo

12
Это действительно хорошая альтернатива. Я только хочу предложить переименовать, чтобы ToEnumerableизбежать путаницыSystem.Linq.Enumerable.AsEnumerable
Fed

3
Как примечание, уже существует ToEnumerableметод расширения для IObservable<T>, так что это имя метода также мешает существующим соглашениям. Честно говоря, я бы предложил вообще не использовать метод расширения просто потому, что он слишком общий.
cwharris

14

Этот вспомогательный метод работает для элемента или многих.

public static IEnumerable<T> ToEnumerable<T>(params T[] items)
{
    return items;
}    

1
Полезный вариант, поскольку он поддерживает более одного элемента. Мне нравится, что он опирается на paramsсоздание массива, поэтому полученный код выглядит чисто. Не решил, нравится ли мне это больше или меньше, чем new T[]{ item1, item2, item3 };для нескольких предметов.
ToolmakerSteve

Умный ! Люблю это
Mitsuru Furuta

10

Я немного удивлен, что никто не предложил новую перегрузку метода с аргументом типа T для упрощения клиентского API.

public void DoSomething<T>(IEnumerable<T> list)
{
    // Do Something
}

public void DoSomething<T>(T item)
{
    DoSomething(new T[] { item });
}

Теперь ваш клиентский код может просто сделать это:

MyItem item = new MyItem();
Obj.DoSomething(item);

или со списком:

List<MyItem> itemList = new List<MyItem>();
Obj.DoSomething(itemList);

19
Более того, вы могли бы иметь, DoSomething<T>(params T[] items)что означает, что компилятор будет обрабатывать преобразование из одного элемента в массив. (Это также позволит вам передавать несколько отдельных элементов и, опять же, компилятор будет обрабатывать преобразование их в массив для вас.)
LukeH

7

Либо (как уже было сказано ранее)

MyMethodThatExpectsAnIEnumerable(new[] { myObject });

или

MyMethodThatExpectsAnIEnumerable(Enumerable.Repeat(myObject, 1));

Как примечание, последняя версия также может быть полезна, если вы хотите пустой список анонимного объекта, например

var x = MyMethodThatExpectsAnIEnumerable(Enumerable.Repeat(new { a = 0, b = "x" }, 0));

Спасибо, хотя Enumerable.Repeat является новым в .Net 3.5. Кажется, что он ведет себя аналогично вспомогательному методу, описанному выше.
Groo

7

Как я только что обнаружил и увидел, что пользователь LukeH также предложил, хороший простой способ сделать это заключается в следующем:

public static void PerformAction(params YourType[] items)
{
    // Forward call to IEnumerable overload
    PerformAction(items.AsEnumerable());
}

public static void PerformAction(IEnumerable<YourType> items)
{
    foreach (YourType item in items)
    {
        // Do stuff
    }
}

Этот шаблон позволит вам вызывать одну и ту же функциональность множеством способов: один элемент; несколько элементов (через запятую); массив; список; перечисление и т. д.

Я не уверен на 100% в эффективности использования метода AsEnumerable, но он действительно работает.

Обновление: функция AsEnumerable выглядит довольно эффективно! ( ссылка )


На самом деле, вам не нужно .AsEnumerable()вообще. Массив YourType[]уже реализует IEnumerable<YourType>. Но мой вопрос касался случая, когда доступен только второй метод (в вашем примере), и вы используете .NET 2.0, и вы хотите передать один элемент.
Groo

2
Да, это так, но вы обнаружите, что вы можете получить переполнение стека ;-)
teppicymon

И чтобы на самом деле ответить на ваш реальный вопрос (а не тот, на который я на самом деле рассчитывал ответить сам за себя!), Да, вы в значительной степени должны преобразовать его в массив в соответствии с ответом Марио
teppicymon

2
Как насчет определения IEnumerable<T> MakeEnumerable<T>(params T[] items) {return items;}метода? Затем можно использовать это с любым ожидаемым IEnumerable<T>, для любого количества отдельных предметов. Код должен быть по сути таким же эффективным, как и определение специального класса для возврата одного элемента.
суперкат

6

Хотя для одного метода это излишне, я считаю, что некоторые люди могут найти интерактивные расширения полезными.

Интерактивные расширения (Ix) от Microsoft включают в себя следующий метод.

public static IEnumerable<TResult> Return<TResult>(TResult value)
{
    yield return value;
}

Который можно использовать так:

var result = EnumerableEx.Return(0);

Ix добавляет новые функциональные возможности, которых нет в оригинальных методах расширения Linq, и является прямым результатом создания Reactive Extensions (Rx).

Подумайте, Linq Extension Methods+ Ix= Rxдля IEnumerable.

Вы можете найти Rx и Ix на CodePlex .


5

Это 30% быстрее , чем yieldили Enumerable.Repeatпри использовании в foreachсвязи с этим C # оптимизации компилятора и того же выступления в других случаях.

public struct SingleSequence<T> : IEnumerable<T> {
    public struct SingleEnumerator : IEnumerator<T> {
        private readonly SingleSequence<T> _parent;
        private bool _couldMove;
        public SingleEnumerator(ref SingleSequence<T> parent) {
            _parent = parent;
            _couldMove = true;
        }
        public T Current => _parent._value;
        object IEnumerator.Current => Current;
        public void Dispose() { }

        public bool MoveNext() {
            if (!_couldMove) return false;
            _couldMove = false;
            return true;
        }
        public void Reset() {
            _couldMove = true;
        }
    }
    private readonly T _value;
    public SingleSequence(T value) {
        _value = value;
    }
    public IEnumerator<T> GetEnumerator() {
        return new SingleEnumerator(ref this);
    }
    IEnumerator IEnumerable.GetEnumerator() {
        return new SingleEnumerator(ref this);
    }
}

в этом тесте:

    // Fastest among seqs, but still 30x times slower than direct sum
    // 49 mops vs 37 mops for yield, or c.30% faster
    [Test]
    public void SingleSequenceStructForEach() {
        var sw = new Stopwatch();
        sw.Start();
        long sum = 0;
        for (var i = 0; i < 100000000; i++) {
            foreach (var single in new SingleSequence<int>(i)) {
                sum += single;
            }
        }
        sw.Stop();
        Console.WriteLine($"Elapsed {sw.ElapsedMilliseconds}");
        Console.WriteLine($"Mops {100000.0 / sw.ElapsedMilliseconds * 1.0}");
    }

Спасибо, имеет смысл создать структуру для этого случая, чтобы уменьшить нагрузку на GC в тесных циклах.
Groo

1
И перечислитель, и перечислимый будут упакованы, когда вернутся ...
Стивен Дрю

2
Не сравнивайте, используя секундомер. Используйте Benchmark.NET github.com/dotnet/BenchmarkDotNet
NN_

1
Только что измерил и new [] { i }вариант примерно в 3,2 раза быстрее, чем ваш вариант. Также ваш вариант примерно в 1,2 раза быстрее, чем расширение с помощью yield return.
Lorond

Вы можете ускорить это, используя другую оптимизацию компилятора C #: добавьте метод, SingleEnumerator GetEnumerator()который возвращает вашу структуру. Компилятор C # будет использовать этот метод (он ищет его с помощью утки) вместо интерфейсных и, таким образом, избегает упаковок. Многие встроенные коллекции используют этот прием, как List . Примечание: это будет работать только тогда, когда у вас есть прямая ссылка SingleSequence, как в вашем примере. Если вы сохраните его в IEnumerable<>переменной, то хитрость не сработает (будет использован метод интерфейса)
enzi

5

IanG имеет хороший пост на эту тему , предлагая EnumerableFrom()в качестве названия (и упоминает, что Haskell и Rx называют его Return). IIRC F # также называет это Return . F # Seqзвонит операторуsingleton<'T> .

Заманчиво, если вы готовы быть C # -центричным, это назвать это Yield[намекая на yield returnвовлеченных в реализацию этого].

Если вы заинтересованы в его аспектах, у Джеймса Майкла Хэра также есть сообщение с нулевым или одним сообщением, которое стоит проверить.


4

Это может быть не лучше, но это круто:

Enumerable.Range(0, 1).Select(i => item);

Это не. Это другое.
nawfal

Ewww. Это много деталей, просто чтобы превратить предмет в перечисляемый.
ToolmakerSteve

1
Это эквивалентно использованию машины Rube Goldberg для приготовления завтрака. Да, это работает, но он прыгает через 10 обручей, чтобы добраться туда. Простая вещь, подобная этой, может быть узким местом в производительности, когда выполняется в узком цикле, который выполняется миллионы раз. На практике аспект производительности не имеет значения в 99% случаев, но лично я все же считаю, что это излишнее излишнее излишество.
Энзи

4

Я согласен с комментариями @ EarthEngine к исходному сообщению о том, что AsSingleton - лучшее название. Смотрите эту запись в Википедии . Тогда из определения синглтона следует, что если в качестве аргумента передается нулевое значение, то AsSingleton должен возвращать IEnumerable с единственным нулевым значением вместо пустого IEnumerable, который бы разрешил if (item == null) yield break;спор. Я думаю, что лучшее решение - использовать два метода: «AsSingleton» и «AsSingletonOrEmpty»; где, в случае если в качестве аргумента передается значение NULL, AsSingleton возвращает одно значение NULL, а AsSingletonOrEmpty возвращает пустой объект IEnumerable. Нравится:

public static IEnumerable<T> AsSingletonOrEmpty<T>(this T source)
{
    if (source == null)
    {
        yield break;
    }
    else
    {
        yield return source;
    }
}

public static IEnumerable<T> AsSingleton<T>(this T source)
{
    yield return source;
}

Тогда они, более или менее, будут аналогичны методам расширения 'First' и 'FirstOrDefault' в IEnumerable, который кажется правильным.


2

Самый простой способ, которым я бы сказал, был бы new T[]{item};; нет синтаксиса, чтобы сделать это. Самым близким эквивалентом, который я могу придумать, является paramsключевое слово, но, разумеется, оно требует от вас доступа к определению метода и может использоваться только с массивами.


2
Enumerable.Range(1,1).Select(_ => {
    //Do some stuff... side effects...
    return item;
});

Приведенный выше код полезен при использовании как

var existingOrNewObject = MyData.Where(myCondition)
       .Concat(Enumerable.Range(1,1).Select(_ => {
           //Create my object...
           return item;
       })).Take(1).First();

В приведенном выше фрагменте кода проверка пустых / пустых значений отсутствует, и гарантируется, что будет возвращен только один объект, не опасаясь исключений. Кроме того, поскольку оно ленивое, закрытие не будет выполняться, пока не будет доказано, что существующие данные не соответствуют критериям.


Этот ответ был автоматически помечен как «низкое качество». Как объясняется в справке («Краткость приемлема, но более полные объяснения лучше.»), Пожалуйста, отредактируйте ее, чтобы сообщить оператору, что он делает неправильно, о чем ваш код.
Сандра Росси

Я вижу, что большая часть этого ответа, включая часть, на которую я отвечаю, была позже отредактирована кем-то другим. но если цель второго фрагмента кода, чтобы обеспечить значение по умолчанию , если MyData.Where(myCondition)пусто, то это уже возможно (и проще) с DefaultIfEmpty(): var existingOrNewObject = MyData.Where(myCondition).DefaultIfEmpty(defaultValue).First();. Это может быть еще более упрощено, var existingOrNewObject = MyData.FirstOrDefault(myCondition);если вы хотите, default(T)а не пользовательское значение.
Бекон

0

Я немного опоздал на вечеринку, но все равно поделюсь своим путем. Моя проблема заключалась в том, что я хотел связать ItemSource или WPF TreeView с одним объектом. Иерархия выглядит так:

Проект> Участок (ы)> Комната (ы)

Всегда должен был быть только один проект, но я все же хотел показать проект в дереве, не передавая коллекцию только с одним объектом, как предлагали некоторые.
Поскольку вы можете передавать только объекты IEnumerable как ItemSource, я решил сделать свой класс IEnumerable:

public class ProjectClass : IEnumerable<ProjectClass>
{
    private readonly SingleItemEnumerator<AufmassProjekt> enumerator;

    ... 

    public IEnumerator<ProjectClass > GetEnumerator() => this.enumerator;

    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

И создайте свой собственный перечислитель соответственно:

public class SingleItemEnumerator : IEnumerator
{
    private bool hasMovedOnce;

    public SingleItemEnumerator(object current)
    {
        this.Current = current;
    }

    public bool MoveNext()
    {
        if (this.hasMovedOnce) return false;
        this.hasMovedOnce = true;
        return true;
    }

    public void Reset()
    { }

    public object Current { get; }
}

public class SingleItemEnumerator<T> : IEnumerator<T>
{
    private bool hasMovedOnce;

    public SingleItemEnumerator(T current)
    {
        this.Current = current;
    }

    public void Dispose() => (this.Current as IDisposable).Dispose();

    public bool MoveNext()
    {
        if (this.hasMovedOnce) return false;
        this.hasMovedOnce = true;
        return true;
    }

    public void Reset()
    { }

    public T Current { get; }

    object IEnumerator.Current => this.Current;
}

Вероятно, это не самое «чистое» решение, но оно сработало для меня.

РЕДАКТИРОВАТЬ
Чтобы поддержать принцип единственной ответственности, как отметил @Groo, я создал новый класс-обертку:

public class SingleItemWrapper : IEnumerable
{
    private readonly SingleItemEnumerator enumerator;

    public SingleItemWrapper(object item)
    {
        this.enumerator = new SingleItemEnumerator(item);
    }

    public object Item => this.enumerator.Current;

    public IEnumerator GetEnumerator() => this.enumerator;
}

public class SingleItemWrapper<T> : IEnumerable<T>
{
    private readonly SingleItemEnumerator<T> enumerator;

    public SingleItemWrapper(T item)
    {
        this.enumerator = new SingleItemEnumerator<T>(item);
    }

    public T Item => this.enumerator.Current;

    public IEnumerator<T> GetEnumerator() => this.enumerator;

    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

Который я использовал так

TreeView.ItemSource = new SingleItemWrapper(itemToWrap);

РЕДАКТИРОВАТЬ 2
Я исправил ошибку с MoveNext()методом.


SingleItemEnumerator<T>Класс имеет смысл, но делает класс «единый элемент IEnumerableсам по себе» , кажется , как нарушение одного принципа ответственности. Возможно, это делает его более практичным, но я все равно предпочитаю оборачивать его по мере необходимости.
Groo

0

Чтобы быть поданным под "Не обязательно хорошим решением, но все же ... решением" или "Тупыми хитростями LINQ", вы могли бы объединиться Enumerable.Empty<>()с Enumerable.Append<>()...

IEnumerable<string> singleElementEnumerable = Enumerable.Empty<string>().Append("Hello, World!");

... или Enumerable.Prepend<>()...

IEnumerable<string> singleElementEnumerable = Enumerable.Empty<string>().Prepend("Hello, World!");

Последние два метода доступны начиная с .NET Framework 4.7.1 и .NET Core 1.0.

Это осуществимое решение , если один были действительно намерены использовать существующие методы вместо того , чтобы писать свои собственные, хотя я в нерешительности , если это более или менее ясно , чем в Enumerable.Repeat<>()растворе . Это определенно более длинный код (отчасти из-за невозможности вывода параметров типа Empty<>()), однако он создает в два раза больше объектов перечислителя.

Завершая это "Знаете ли вы, что эти методы существуют?" Ответ, Array.Empty<>()можно заменить Enumerable.Empty<>(), но трудно утверждать, что делает ситуацию ... лучше.


0

Я недавно спросил то же самое в другом посте ( есть ли способ вызвать метод C #, требующий IEnumerable <T> с одним значением? ... с тестированием производительности ).

Я хотел, чтобы люди заглянули сюда, чтобы увидеть краткое сравнение производительности, показанное в этом новом сообщении для 4 подходов, представленных в этих ответах.

Кажется, что простая запись new[] { x }аргументов метода - самое короткое и быстрое решение.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.