วิธีรับดัชนีขององค์ประกอบใน IEnumerable?


144

ฉันเขียนสิ่งนี้:

public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> obj, T value)
    {
        return obj
            .Select((a, i) => (a.Equals(value)) ? i : -1)
            .Max();
    }

    public static int IndexOf<T>(this IEnumerable<T> obj, T value
           , IEqualityComparer<T> comparer)
    {
        return obj
            .Select((a, i) => (comparer.Equals(a, value)) ? i : -1)
            .Max();
    }
}

แต่ฉันไม่รู้ว่ามันมีอยู่แล้วใช่ไหม?


4
ปัญหาเกี่ยวกับการให้Maxวิธีการก็คือว่ามันช่วยให้มองและ b: มันส่งกลับที่ผ่านมาดัชนีเมื่อมีซ้ำกัน (คนมักจะคาดหวังว่าดัชนีแรก)
Marc Gravell

1
geekswithblogs.netเปรียบเทียบ 4 โซลูชั่นและประสิทธิภาพ ToList()/FindIndex()ดำเนินเคล็ดลับที่ดีที่สุด
nixda

คำตอบ:


51

จุดรวมของการกำจัดสิ่งต่าง ๆ ออกมาในรูปแบบของ IEnumerable นั้นทำให้คุณสามารถทำซ้ำเนื้อหาได้อย่างเกียจคร้าน เป็นเช่นนี้มีไม่ได้จริงๆแนวคิดของดัชนี สิ่งที่คุณกำลังทำจริงๆไม่สมเหตุสมผลสำหรับ IEnumerable หากคุณต้องการสิ่งที่รองรับการเข้าถึงโดยดัชนีให้ใส่ไว้ในรายการหรือคอลเล็กชันจริง


8
ขณะนี้ฉันเข้ามาที่เธรดนี้เนื่องจากฉันกำลังใช้ IList <> wrapper สำหรับประเภท IEnumerable <> เพื่อใช้ IEnumerable <> วัตถุของฉันกับองค์ประกอบของบุคคลที่สามซึ่งรองรับแหล่งข้อมูลประเภท IList เท่านั้น ฉันยอมรับว่าการพยายามหาดัชนีขององค์ประกอบภายในวัตถุ IEnumerable นั้นโดยส่วนใหญ่แล้วสัญญาณของสิ่งที่ถูกทำผิดมีหลายครั้งที่การค้นหาดัชนีดังกล่าวเมื่อเต้นทำซ้ำคอลเลกชันขนาดใหญ่ในหน่วยความจำเพียงเพื่อประโยชน์ในการค้นหาดัชนี ขององค์ประกอบเดียวเมื่อคุณมี IEnumerable
jpierson

215
-1 สาเหตุ: IEnumerable<>มีเหตุผลที่ถูกต้องว่าทำไมคุณต้องการที่จะได้รับดัชนีออกจากเป็น ฉันไม่ได้ซื้อ "คุณจะทำสิ่งนี้" ทั้งหมด
John Alexiou

78
เห็นด้วยกับ @ ja72; หากคุณไม่ควรจัดการกับดัชนีด้วยIEnumerableก็Enumerable.ElementAtจะไม่มีอยู่จริง IndexOfเป็นเพียงผกผัน - ข้อโต้แย้งใด ๆ ElementAtกับมันต้องใช้อย่างเท่าเทียมกัน
Kirk Woll

7
เคลียร์ C # คิดถึงแนวคิดของ IIndexableEnumerable ที่จะเทียบเท่ากับแนวคิด "เข้าถึงได้แบบสุ่ม" ในคำศัพท์ C ++ STL
v.oddou

14
ส่วนขยายที่มีการโอเวอร์โหลดเช่น Select ((x, i) => ... ) ดูเหมือนจะบ่งบอกว่าดัชนีเหล่านี้ควรมีอยู่
Michael

126

ฉันถามภูมิปัญญา แต่บางที:

source.TakeWhile(x => x != value).Count();

(ใช้EqualityComparer<T>.Defaultเพื่อเลียนแบบ!=ถ้าจำเป็น) - แต่คุณต้องดูกลับ -1 ถ้าไม่พบ ... ดังนั้นบางทีมันอาจจะเป็นทางยาว

public static int IndexOf<T>(this IEnumerable<T> source, T value)
{
    int index = 0;
    var comparer = EqualityComparer<T>.Default; // or pass in as a parameter
    foreach (T item in source)
    {
        if (comparer.Equals(item, value)) return index;
        index++;
    }
    return -1;
}

8
+1 สำหรับ "การตั้งคำถามกับภูมิปัญญา" 9 ครั้งจาก 10 อาจเป็นความคิดที่ไม่ดีในตอนแรก
Joel Coehoorn

โซลูชันลูปที่ชัดเจนยังทำงานได้เร็วขึ้น 2x (ในกรณีที่แย่ที่สุด) กว่าโซลูชัน Select () แม็กซ์ () เช่นกัน
Steve Guidi

1
คุณสามารถนับองค์ประกอบโดยแลมบ์ดาได้โดยไม่ต้องใช้ TakeWhile - ซึ่งจะช่วยประหยัดหนึ่งลูป: source.Count (x => x! = value);
Kamarey

10
@Kamarey - ไม่นั่นเป็นสิ่งที่แตกต่าง TakeWhile หยุดเมื่อมันได้รับความล้มเหลว; Count (predicate) ส่งคืนค่าที่ตรงกัน เช่นถ้าคนแรกพลาดและทุกอย่างอื่นเป็นจริง TakeWhile (pred) .Count () จะรายงาน 0; จำนวน (pred) จะรายงาน n-1
Marc Gravell

1
TakeWhileฉลาด! โปรดจำไว้ว่าสิ่งนี้จะส่งคืนCountหากองค์ประกอบไม่มีอยู่ซึ่งเป็นความเบี่ยงเบนจากพฤติกรรมมาตรฐาน
nawfal

27

ฉันจะใช้มันเช่นนี้

public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> obj, T value)
    {
        return obj.IndexOf(value, null);
    }

    public static int IndexOf<T>(this IEnumerable<T> obj, T value, IEqualityComparer<T> comparer)
    {
        comparer = comparer ?? EqualityComparer<T>.Default;
        var found = obj
            .Select((a, i) => new { a, i })
            .FirstOrDefault(x => comparer.Equals(x.a, value));
        return found == null ? -1 : found.i;
    }
}

1
ที่จริงแล้วน่ารักมาก +1 มันเกี่ยวข้องกับวัตถุพิเศษ แต่ควรจะค่อนข้างถูก (GEN0) ดังนั้นจึงไม่ใช่ปัญหาใหญ่ ==อาจต้องทำงานหรือไม่
Marc Gravell

1
เพิ่ม IEqualityComparer overload ในรูปแบบ LINQ จริง ;)
dahlbyk

1
ฉันคิดว่าคุณตั้งใจจะพูดว่า ... comparer.Equals (xa, value) =)
Marc

เนื่องจากนิพจน์เลือกส่งคืนผลลัพธ์ที่รวมกันซึ่งถูกประมวลผลแล้วฉันจินตนาการว่าใช้ประเภทค่า KeyValuePair อย่างชัดเจนจะช่วยให้คุณหลีกเลี่ยงการจัดสรรฮีปใด ๆ ได้ตราบใดที่. NET แปลค่าประเภทสแต็กบนสแต็กและ เครื่องรัฐใด ๆ ที่ LINQ อาจสร้างใช้เขตข้อมูลสำหรับผล Select'd ซึ่งไม่ได้ประกาศว่าเป็นวัตถุเปล่า (จึงทำให้ผลลัพธ์ KVP เพื่อรับกล่อง) แน่นอนคุณต้องทำใหม่พบเงื่อนไขว่าง == (เนื่องจากพบว่าตอนนี้จะเป็นค่า KVP) อาจจะใช้ DefaultIfEmpty () หรือKVP<T, int?>(nullable index)
kornman00

1
การใช้งานที่ดีแม้ว่าสิ่งหนึ่งที่ฉันขอแนะนำให้เพิ่มคือการตรวจสอบเพื่อดูว่า obj ใช้หรือไม่IList<T>และหากเป็นเช่นนั้นให้เลื่อนไปที่วิธี IndexOf ในกรณีที่มีการปรับให้เหมาะสมกับประเภทเฉพาะ
Josh

16

วิธีที่ฉันทำอยู่ในขณะนี้สั้นกว่าที่แนะนำเล็กน้อยและเท่าที่ฉันจะบอกได้ก็ให้ผลลัพธ์ที่ต้องการ:

 var index = haystack.ToList().IndexOf(needle);

มันค่อนข้างเงียบ แต่ก็ทำหน้าที่ได้ค่อนข้างกระชับ


6
แม้ว่านี่จะใช้ได้กับคอลเล็กชั่นเล็ก ๆ น้อย ๆ แต่คุณมีสินค้านับล้านรายการใน "กองหญ้า" การทำ ToList () ในนั้นจะวนซ้ำองค์ประกอบทั้งหมดหนึ่งล้านและเพิ่มเข้าไปในรายการ จากนั้นจะค้นหารายการเพื่อค้นหาดัชนีขององค์ประกอบที่ตรงกัน นี่จะไม่มีประสิทธิภาพอย่างมากเช่นเดียวกับความเป็นไปได้ของการโยนข้อยกเว้นหากรายการมีขนาดใหญ่เกินไป
esteuart

3
@esteuart Definitely - คุณต้องเลือกวิธีการที่เหมาะสมกับกรณีการใช้งานของคุณ ฉันสงสัยว่ามีขนาดเดียวที่เหมาะกับโซลูชันทั้งหมดซึ่งอาจเป็นสาเหตุที่ไม่มีการนำไปใช้ในไลบรารีหลัก
Mark Watts

8

วิธีที่ดีที่สุดในการรับตำแหน่งคือโดยFindIndexฟังก์ชั่นนี้ใช้ได้เฉพาะสำหรับList<>

ตัวอย่าง

int id = listMyObject.FindIndex(x => x.Id == 15); 

หากคุณมีตัวแจงนับหรืออาร์เรย์ใช้วิธีนี้

int id = myEnumerator.ToList().FindIndex(x => x.Id == 15); 

หรือ

 int id = myArray.ToList().FindIndex(x => x.Id == 15); 

7

ฉันคิดว่าตัวเลือกที่ดีที่สุดคือการใช้เช่นนี้

public static int IndexOf<T>(this IEnumerable<T> enumerable, T element, IEqualityComparer<T> comparer = null)
{
    int i = 0;
    comparer = comparer ?? EqualityComparer<T>.Default;
    foreach (var currentElement in enumerable)
    {
        if (comparer.Equals(currentElement, element))
        {
            return i;
        }

        i++;
    }

    return -1;
}

มันจะไม่สร้างวัตถุที่ไม่ระบุชื่อ


5

สายในเกมฉันรู้ ... แต่นี่คือสิ่งที่ฉันเพิ่งทำ มันแตกต่างจากของคุณเล็กน้อย แต่อนุญาตให้โปรแกรมเมอร์กำหนดสิ่งที่การดำเนินการความเท่าเทียมกันจะต้อง (ภาคแสดง) ซึ่งฉันพบว่ามีประโยชน์มากเมื่อจัดการกับประเภทที่แตกต่างกันเนื่องจากฉันมีวิธีทั่วไปในการทำโดยไม่คำนึงถึงประเภทของวัตถุและ<T>สร้างขึ้นในตัวดำเนินการความเท่าเทียมกัน

นอกจากนี้ยังมีหน่วยความจำขนาดเล็กมากและรวดเร็วมาก / มีประสิทธิภาพ ... ถ้าคุณเป็นห่วง

ที่แย่กว่านั้นคือคุณจะเพิ่มส่วนนี้ลงในรายการส่วนขยายของคุณ

อย่างไรก็ตาม ... นี่ไง

 public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
 {
     int retval = -1;
     var enumerator = source.GetEnumerator();

     while (enumerator.MoveNext())
     {
         retval += 1;
         if (predicate(enumerator.Current))
         {
             IDisposable disposable = enumerator as System.IDisposable;
             if (disposable != null) disposable.Dispose();
             return retval;
         }
     }
     IDisposable disposable = enumerator as System.IDisposable;
     if (disposable != null) disposable.Dispose();
     return -1;
 }

หวังว่านี่จะช่วยใครซักคน


1
บางทีฉันอาจจะพลาดบางสิ่งบางอย่าง แต่ทำไมGetEnumeratorและMoveNextมากกว่าแค่foreach?
Josh Gallagher

1
คำตอบสั้น ๆ ? อย่างมีประสิทธิภาพ คำตอบที่ยาว: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv

2
มองไปที่อิลลินอยส์ปรากฏว่าแตกต่างประสิทธิภาพคือการที่foreachจะเรียกในการแจงนับถ้าการดำเนินการDispose IDisposable(ดูstackoverflow.com/questions/4982396/… ) เนื่องจากรหัสในคำตอบนี้ไม่ทราบว่าผลลัพธ์ของการโทรGetEnumeratorเป็นหรือไม่ได้ทิ้งแล้วควรทำเช่นเดียวกัน ณ จุดนี้ฉันยังไม่ชัดเจนว่ามีประโยชน์อย่างสมบูรณ์แม้ว่าจะมี IL พิเศษบางอย่างที่มีวัตถุประสงค์ไม่ได้กระโดดออกมาที่ฉัน!
Josh Gallagher

@JoshGallagher ฉันได้ทำการวิจัยสักครู่กลับมาเกี่ยวกับประโยชน์ที่สมบูรณ์แบบระหว่าง foreach และ (i) และประโยชน์หลักของการใช้ for (i) คือ ByRefs วัตถุในสถานที่แทนที่จะสร้างมันขึ้นใหม่ / ส่งผ่านมัน กลับ ByVal ฉันจะถือว่าเหมือนกันใช้กับ MoveNext กับ foreach แต่ฉันไม่แน่ใจเกี่ยวกับอันนั้น บางทีพวกเขาทั้งคู่อาจใช้ ByVal ...
MaxOvrdrv

2
อ่านบล็อกนี้ ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/ ...... ) อาจเป็นได้ว่า "วนซ้ำวนซ้ำ" ซึ่งเขาอ้างถึงคือforeachลูปซึ่งในกรณีนี้สำหรับกรณีเฉพาะของTเป็นประเภทค่ามันอาจจะบันทึกการดำเนินการกล่อง / unbox โดยใช้ห่วงขณะที่ แต่นี้ไม่ได้เป็นลมออกโดย IL foreachผมได้จากรุ่นของคำตอบของคุณด้วย ฉันยังคงคิดว่าการกำจัด iterator แบบมีเงื่อนไขเป็นสิ่งสำคัญ คุณสามารถแก้ไขคำตอบเพื่อรวมสิ่งนั้นได้หรือไม่?
Josh Gallagher

5

ไม่กี่ปีต่อมา แต่สิ่งนี้ใช้ Linq ส่งคืน -1 หากไม่พบไม่สร้างวัตถุพิเศษและควรลัดวงจรเมื่อพบ [เมื่อเทียบกับการวนซ้ำทั่ว IEnumerable ทั้งหมด:

public static int IndexOf<T>(this IEnumerable<T> list, T item)
{
    return list.Select((x, index) => EqualityComparer<T>.Default.Equals(item, x)
                                     ? index
                                     : -1)
               .FirstOr(x => x != -1, -1);
}

ที่ 'FirstOr' อยู่ที่ไหน:

public static T FirstOr<T>(this IEnumerable<T> source, T alternate)
{
    return source.DefaultIfEmpty(alternate)
                 .First();
}

public static T FirstOr<T>(this IEnumerable<T> source, Func<T, bool> predicate, T alternate)
{
    return source.Where(predicate)
                 .FirstOr(alternate);
}

อีกวิธีในการทำคือ: public static int IndexOf <T> (รายการ IENumerable <T> รายการ T) {int e = list.Select ((x, ดัชนี) => EqualityComparer <T> .Default.Equals ( รายการ x)? x + 1: -1) .FirstOrDefault (x => x> 0); ส่งคืน (e == 0) หรือไม่ -1: e - 1); }
Anu Thomas Chandy

"ไม่สร้างวัตถุพิเศษ" ในความเป็นจริง Linq จะสร้างวัตถุในพื้นหลังจึงไม่ถูกต้องอย่างสมบูรณ์ ทั้งสองsource.Whereและsource.DefaultIfEmptyตัวอย่างจะสร้างIEnumerableแต่ละ
Martin Odhelius

1

เจอสิ่งนี้ในวันนี้เพื่อค้นหาคำตอบและฉันคิดว่าฉันจะเพิ่มเวอร์ชันของฉันลงในรายการ มันใช้ตัวดำเนินการ null แบบไม่มีเงื่อนไขของ c # 6.0

IEnumerable<Item> collection = GetTheCollection();

var index = collection
.Select((item,idx) => new { Item = item, Index = idx })
//or .FirstOrDefault(_ =>  _.Item.Prop == something)
.FirstOrDefault(_ => _.Item == itemToFind)?.Index ?? -1;

ฉันได้ทำบาง 'แข่งม้าเก่า' (การทดสอบ) และสำหรับคอลเลกชันขนาดใหญ่ (~ 100,000) กรณีสถานการณ์ที่เลวร้ายที่สุดที่รายการที่คุณต้องการเป็นที่สิ้นสุดนี้เป็น2 เท่าToList().FindIndex()ได้เร็วขึ้นกว่าการทำ หากรายการที่คุณต้องการอยู่ตรงกลางมันจะเร็วกว่า~ 4x

สำหรับคอลเลกชันขนาดเล็ก (~ 10,000) ดูเหมือนว่าจะเร็วขึ้นเพียงเล็กน้อยเท่านั้น

นี่คือวิธีที่ฉันทดสอบมันhttps://gist.github.com/insulind/16310945247fcf13ba186a45734f254e


1

ใช้คำตอบของ @Marc Gravell ฉันพบวิธีใช้วิธีต่อไปนี้:

source.TakeWhile(x => x != value).Count();

เพื่อรับ -1 เมื่อไม่พบรายการ:

internal static class Utils
{

    public static int IndexOf<T>(this IEnumerable<T> enumerable, T item) => enumerable.IndexOf(item, EqualityComparer<T>.Default);

    public static int IndexOf<T>(this IEnumerable<T> enumerable, T item, EqualityComparer<T> comparer)
    {
        int index = enumerable.TakeWhile(x => comparer.Equals(x, item)).Count();
        return index == enumerable.Count() ? -1 : index;
    }
}

ฉันคิดว่าวิธีนี้อาจเป็นได้ทั้งเร็วและง่ายกว่า อย่างไรก็ตามฉันยังไม่ได้ทดสอบการแสดง


0

อีกทางเลือกหนึ่งในการค้นหาดัชนีหลังจากข้อเท็จจริงคือการห่อ Enumerable ค่อนข้างคล้ายกับการใช้วิธี Linq GroupBy ()

public static class IndexedEnumerable
{
    public static IndexedEnumerable<T> ToIndexed<T>(this IEnumerable<T> items)
    {
        return IndexedEnumerable<T>.Create(items);
    }
}

public class IndexedEnumerable<T> : IEnumerable<IndexedEnumerable<T>.IndexedItem>
{
    private readonly IEnumerable<IndexedItem> _items;

    public IndexedEnumerable(IEnumerable<IndexedItem> items)
    {
        _items = items;
    }

    public class IndexedItem
    {
        public IndexedItem(int index, T value)
        {
            Index = index;
            Value = value;
        }

        public T Value { get; private set; }
        public int Index { get; private set; }
    }

    public static IndexedEnumerable<T> Create(IEnumerable<T> items)
    {
        return new IndexedEnumerable<T>(items.Select((item, index) => new IndexedItem(index, item)));
    }

    public IEnumerator<IndexedItem> GetEnumerator()
    {
        return _items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

ซึ่งให้กรณีการใช้งานของ:

var items = new[] {1, 2, 3};
var indexedItems = items.ToIndexed();
foreach (var item in indexedItems)
{
    Console.WriteLine("items[{0}] = {1}", item.Index, item.Value);
}

พื้นฐานที่ดี มันจะมีประโยชน์ในการเพิ่มสมาชิก IsEven, IsOdd, IsFirst และ IsLast เช่นกัน
JJS

0

สิ่งนี้จะเจ๋งจริง ๆ ด้วยส่วนขยาย (ทำงานเป็นพร็อกซี) ตัวอย่างเช่น:

collection.SelectWithIndex(); 
// vs. 
collection.Select((item, index) => item);

ซึ่งโดยอัตโนมัติจะกำหนดดัชนีให้กับคอลเลกชันที่สามารถเข้าถึงได้ผ่านทางนี้ Indexคุณสมบัตินี้

อินเตอร์เฟซ:

public interface IIndexable
{
    int Index { get; set; }
}

ส่วนขยายที่กำหนดเอง (อาจมีประโยชน์มากที่สุดสำหรับการทำงานกับ EF และ DbContext):

public static class EnumerableXtensions
{
    public static IEnumerable<TModel> SelectWithIndex<TModel>(
        this IEnumerable<TModel> collection) where TModel : class, IIndexable
    {
        return collection.Select((item, index) =>
        {
            item.Index = index;
            return item;
        });
    }
}

public class SomeModelDTO : IIndexable
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public int Index { get; set; }
}

// In a method
var items = from a in db.SomeTable
            where a.Id == someValue
            select new SomeModelDTO
            {
                Id = a.Id,
                Name = a.Name,
                Price = a.Price
            };

return items.SelectWithIndex()
            .OrderBy(m => m.Name)
            .Skip(pageStart)
            .Take(pageSize)
            .ToList();
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.