Передача одного елемента як 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>.

[Редагувати] Як метод розширення він може бути названий:

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 monad в Microsoft, я здивований, що щось подібне не в першу чергу.
Метт Енрайт

2
Для методу розширення не слід називати його, AsEnumerableоскільки вбудоване розширення з цим ім'ям вже існує . (Коли Tзнаряддя IEnumerable, напр string.,)
Джон-Ерік

22
Як щодо називання методу Yield? Ніщо не перемагає стислості.
Філіп Гін

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

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

Відповіді:


100

Ваш помічник - це найчистіший спосіб зробити це, ІМО. Якщо ви переходите до списку чи масиву, то недобросовісний фрагмент коду може передати його та змінити вміст, що призведе до незвичайної поведінки в деяких ситуаціях. Ви можете використовувати колекцію лише для читання, але це, ймовірно, передбачає ще більше обгортання. Я думаю, що ваше рішення настільки ж акуратне, як виходить.


добре, якщо список / масив побудований спеціально, його область закінчується після виклику методу, тому це не повинно спричиняти проблем
Mario F

Якщо він надсилається як масив, як це можна змінити? Я здогадуюсь, що це може бути передано до масиву, а потім змінити посилання на щось інше, але що б цього добре? (Мені, мабуть, чогось не вистачає ...)
Свиш

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

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

3
@HamishGrubijan: Це здається, що ви хотіли лише dictionary.Valuesпочати. В основному незрозуміло, що відбувається, але я пропоную вам почати нове питання про це.
Джон Скіт

133

Добре, якщо метод очікує IEnumerable вам доведеться передати щось із списку, навіть якщо він містить лише один елемент.

мимохідь

new[] { item }

як я думаю, що аргументу має бути достатньо


32
Я думаю, ви навіть можете скинути Т, оскільки він буде зроблений (якщо тільки я тут не дуже помиляюся: p)
Свиш

2
Я думаю, що це не виводиться на C # 2.0.
Гроо

4
"Мова - це C #, рамкова версія 2" Компілятор C # 3 може зробити висновок T, і код буде повністю сумісний з .NET 2.
sisve

найкраща відповідь внизу ?!
Фалько Олександр

2
@Svish Я запропонував і відредагував відповідь, щоб зробити саме це - ми зараз у 2020 році, тому це має бути "стандартний" imho
OschtärEi

120

У C # 3.0 ви можете використовувати клас System.Linq.Numerable:

// 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), мені було просто цікаво, чи є для цього вбудований механізм.
Гроо

12
Це дійсно приємна альтернатива. Я тільки хочу запропонувати перейменування , ToEnumerableщоб уникнути балуватися зSystem.Linq.Enumerable.AsEnumerable
Федей

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

Розумний! Любіть це
Міцуру Фурута

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));

Дякую, хоча багато. Повторіть нове в .Net 3.5. Здається, він веде себе подібним до допоміжного методу, описаного вище.
Гроо

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, і ви хочете передати один елемент.
Гроо

2
Так, так, але ви побачите, що ви можете отримати переповнення стека ;-)
teppicymon

І щоб насправді відповісти на ваше справжнє запитання (не на те, на що я насправді хотів відповісти сам!), Так, вам, як
правило,

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

6

Хоча це є зайвим для одного методу, я вважаю, що інтерактивне розширення може виявитися корисним.

Інтерактивне розширення (Ix) від Microsoft включає наступний метод.

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

Які можна використовувати так:

var result = EnumerableEx.Return(0);

Ix додає нову функціональність, яку не можна знайти в оригінальних методах розширення Linq, і є прямим результатом створення реактивних розширень (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 у вузьких петлях.
Гроо

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 # називає це також Повернення . F # ' Seqдзвонить операторуsingleton<'T> .

Заманливо, якщо ви готові бути C # -центричним - це називати його Yield[натякаючи наyield return учасників його реалізації].

Якщо ви зацікавлені в перфторних аспектах цього, Джеймс Майкл Заєць має також повернення нуля або один пункт, який також варто перевірити.


4

Це може бути не краще, але це якось круто:

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

Це не. Це інакше.
nawfal

Ewww Це багато деталей, просто щоб перетворити предмет на безліч.
ToolmakerSteve

1
Це еквівалент використання машини Rube Goldberg для приготування сніданку. Так, це працює, але він проскакує через 10 обручів, щоб потрапити туди. Проста річ, як це, може бути вузьким місцем продуктивності, якщо це зроблено в тісному циклі, який виконується мільйони разів. На практиці аспект продуктивності не має значення в 99% випадків, але особисто я все-таки вважаю, що це непотрібне зайве перевиконання.
enzi

4

Я погоджуюся з коментарями @ EarthEngine до оригінальної публікації, яка полягає в тому, що "AsSingleton" є кращою назвою. Дивіться цей запис у Вікіпедії . Тоді з визначення одиночного випливає, що якщо нульове значення передається як аргумент, що "AsSingleton" повинен повернути IEnumerable з одним нульовим значенням замість порожнього IEnumerable, який би вирішив if (item == null) yield break;дискусію. Я думаю, що найкращим рішенням є два методи: 'AsSingleton' та 'AsSingletonOrEmpty'; де, у випадку, якщо нуль буде передано як аргумент, "AsSingleton" поверне одне нульове значення, а "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();
}

І відповідно створіть мого власного Enumerator:

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;
}

Це, мабуть, не найчистіше рішення, але воно працювало для мене.

EDIT
Щоб підтримувати принцип єдиної відповідальності, як зазначив @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);

EDIT 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.