Làm thế nào để có được chỉ mục của một phần tử trong IEnumerable?


144

Tôi đã viết điều này:

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

Nhưng tôi không biết nếu nó đã tồn tại, phải không?


4
Vấn đề với Maxcách tiếp cận là: nó tiếp tục tìm kiếm và b: nó trả về chỉ mục cuối cùng khi có các bản sao (mọi người thường mong đợi chỉ số đầu tiên)
Marc Gravell

1
geekswithbloss.net so sánh 4 giải pháp và hiệu suất của chúng. Thủ ToList()/FindIndex()thuật này hoạt động tốt nhất
nixda

Câu trả lời:


51

Toàn bộ vấn đề nhận được mọi thứ như IEnumerable là để bạn có thể lặp đi lặp lại một cách lười biếng các nội dung. Như vậy, thực sự không có khái niệm về một chỉ mục. Những gì bạn đang làm thực sự không có nhiều ý nghĩa đối với một IEnumerable. Nếu bạn cần thứ gì đó hỗ trợ truy cập theo chỉ mục, hãy đặt nó vào danh sách hoặc bộ sưu tập thực tế.


8
Hiện tại tôi đã đi qua chủ đề này bởi vì tôi đang triển khai một trình bao bọc IList <> chung cho loại IEnumerable <> để sử dụng các đối tượng IEnumerable <> của tôi với các thành phần bên thứ ba chỉ hỗ trợ các nguồn dữ liệu của loại IList. Tôi đồng ý rằng trong hầu hết các trường hợp, việc cố gắng lấy chỉ mục của một phần tử trong một đối tượng IEnumerable có thể là dấu hiệu của một thứ gì đó được thực hiện sai, đôi khi việc tìm kiếm chỉ mục đó một lần tạo ra một bộ sưu tập lớn trong bộ nhớ chỉ vì mục đích tìm kiếm chỉ mục của một yếu tố khi bạn đã có IEnumerable.
jpierson

214
-1 nguyên nhân: Có những lý do chính đáng tại sao bạn muốn lấy chỉ mục ra khỏi a IEnumerable<>. Tôi không mua toàn bộ giáo điều "bạn sẽ làm điều này".
John Alexiou

78
Đồng ý với @ ja72; nếu bạn không nên làm việc với các chỉ mục IEnumerablethì Enumerable.ElementAtsẽ không tồn tại. IndexOfchỉ đơn giản là nghịch đảo - bất kỳ đối số nào chống lại nó đều phải được áp dụng như nhau ElementAt.
Kirk Woll

7
Cleary, C # bỏ lỡ khái niệm IIndexableEnumerable. đó sẽ chỉ tương đương với một khái niệm "có thể truy cập ngẫu nhiên", theo thuật ngữ C ++ STL.
v.oddou

14
các tiện ích mở rộng có quá tải như Chọn ((x, i) => ...) dường như ngụ ý rằng các chỉ mục này nên tồn tại
Michael

126

Tôi muốn hỏi sự khôn ngoan, nhưng có lẽ:

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

(sử dụng EqualityComparer<T>.Defaultđể mô phỏng !=nếu cần) - nhưng bạn cần xem để trả về -1 nếu không tìm thấy ... vì vậy có lẽ chỉ cần thực hiện theo cách lâu dài

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 cho "đặt câu hỏi về sự khôn ngoan". 9 trong số 10 lần đầu tiên có lẽ là một ý tưởng tồi.
Joel Coehoorn

Giải pháp vòng lặp rõ ràng cũng chạy nhanh gấp 2 lần (trong trường hợp xấu nhất) so với giải pháp Chọn (). Max ().
Steve Guidi

1
Bạn chỉ có thể Đếm các phần tử bằng lambda mà không cần TakeWhile - nó lưu một vòng lặp: source.Count (x => x! = Value);
Kamarey

10
@Kamarey - không, đó là một cái gì đó khác nhau. TakeWhile dừng lại khi gặp thất bại; Đếm (vị ngữ) trả về những cái phù hợp. tức là nếu lần đầu tiên bị bỏ lỡ và mọi thứ khác là đúng, TakeWhile (trước) .Count () sẽ báo cáo 0; Đếm (trước) sẽ báo cáo n-1.
Marc Gravell

1
TakeWhilethật thông minh! Hãy nhớ rằng điều này trả về Countnếu phần tử không tồn tại, đó là sự sai lệch so với hành vi tiêu chuẩn.
nawfal

27

Tôi sẽ thực hiện nó như thế này:

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
Điều đó thực sự rất dễ thương, +1! Nó liên quan đến các đối tượng bổ sung, nhưng chúng nên tương đối rẻ (GEN0), vì vậy không phải là một vấn đề lớn. Có ==thể cần làm việc?
Marc Gravell

1
Đã thêm quá tải IEqualityComparer, theo kiểu LINQ thực sự. ;)
dahlbyk

1
Tôi nghĩ bạn muốn nói ... so sánh.Equals (xa, giá trị) =)
Marc

Do biểu thức Chọn trả về kết quả kết hợp, sau đó được xử lý, tôi tưởng tượng rõ ràng bằng cách sử dụng loại giá trị KeyValuePair sẽ cho phép bạn tránh mọi loại phân bổ heap, miễn là .NET impl cấp phát các loại giá trị trên ngăn xếp và bất kỳ máy trạng thái nào mà LINQ có thể tạo sẽ sử dụng một trường cho kết quả Chọn không được khai báo là Đối tượng trần (do đó khiến kết quả KVP bị đóng hộp). Tất nhiên, bạn phải làm lại điều kiện đã tìm thấy == null (vì tìm thấy bây giờ sẽ là giá trị KVP). Có thể sử dụng KVP<T, int?>
Default IfEmpty

1
Triển khai tốt, mặc dù một điều tôi khuyên bạn nên thêm là kiểm tra xem liệu obj có thực hiện IList<T>hay không và nếu vậy, hãy trì hoãn phương thức IndexOf của nó chỉ trong trường hợp nó có tối ưu hóa theo loại cụ thể.
Josh

16

Cách tôi hiện đang làm điều này ngắn hơn một chút so với những gì đã được đề xuất và theo như tôi có thể nói cho kết quả mong muốn:

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

Đó là một chút lộn xộn, nhưng nó làm công việc và khá súc tích.


6
Mặc dù điều này sẽ làm việc cho các bộ sưu tập nhỏ, giả sử bạn có một triệu vật phẩm trong "haystack". Thực hiện một ToList () trên đó sẽ lặp qua tất cả một triệu phần tử và thêm chúng vào danh sách. Sau đó, nó sẽ tìm kiếm thông qua danh sách để tìm chỉ mục của phần tử phù hợp. Điều này sẽ cực kỳ không hiệu quả cũng như khả năng ném ngoại lệ nếu danh sách trở nên quá lớn.
esteuart

3
@esteuart Chắc chắn - bạn cần chọn một phương pháp phù hợp với trường hợp sử dụng của bạn. Tôi nghi ngờ có một kích thước phù hợp với tất cả các giải pháp, đó có thể là lý do tại sao không có triển khai trong các thư viện cốt lõi.
Đánh dấu Watts

8

Cách tốt nhất để bắt vị trí là FindIndex Chức năng này chỉ khả dụng choList<>

Thí dụ

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

Nếu bạn có liệt kê hoặc mảng sử dụng theo cách này

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

hoặc là

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

7

Tôi nghĩ rằng lựa chọn tốt nhất là thực hiện như thế này:

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

Nó cũng sẽ không tạo đối tượng ẩn danh


5

Một chút muộn trong trò chơi, tôi biết ... nhưng đây là những gì tôi đã làm gần đây. Nó hơi khác so với của bạn, nhưng cho phép lập trình viên ra lệnh cho hoạt động bình đẳng cần là gì (vị ngữ). Mà tôi thấy rất hữu ích khi xử lý các loại khác nhau, vì sau đó tôi có cách làm chung chung bất kể loại đối tượng và<T> được xây dựng trong toán tử đẳng thức.

Nó cũng có dung lượng bộ nhớ rất nhỏ và rất, rất nhanh / hiệu quả ... nếu bạn quan tâm đến điều đó.

Tệ hơn nữa, bạn sẽ chỉ thêm phần này vào danh sách các tiện ích mở rộng của mình.

Dù sao ... đây rồi.

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

Hy vọng điều này sẽ giúp được ai đó.


1
Có lẽ tôi đang thiếu một cái gì đó, nhưng tại sao GetEnumeratorMoveNextthay vì chỉ là một foreach?
Josh Gallagher

1
Câu trả lời ngắn? Hiệu quả. Câu trả lời dài: msdn.microsoft.com/en-us/l
Library / 9yb8xew9.aspx

2
Nhìn vào IL có vẻ như sự khác biệt về hiệu suất là foreachsẽ gọi Disposecho điều tra viên nếu nó thực hiện IDisposable. (Xem stackoverflow.com/questions/4982394/ Mạnh ) Vì mã trong câu trả lời này không biết kết quả của cuộc gọi GetEnumeratorlà hay không dùng một lần nên nó sẽ làm như vậy. Vào thời điểm đó tôi vẫn chưa rõ rằng có một lợi ích hoàn hảo, mặc dù có thêm một số IL mà mục đích của tôi đã không nhảy ra khỏi tôi!
Josh Gallagher

@JoshGallagher Tôi đã nghiên cứu một chút về lợi ích hoàn hảo giữa foreach và for (i), và lợi ích chính của việc sử dụng cho (i) là nó ByRefs đối tượng tại chỗ thay vì tạo lại / chuyển nó trở lại ByVal. Tôi sẽ giả sử điều tương tự áp dụng cho MoveNext so với foreach, nhưng tôi không chắc về điều đó. Có lẽ cả hai đều sử dụng ByVal ...
MaxOvrdrv

2
Đọc blog này ( blog.msdn.com/b/ericlippert/archive/2010/09/30/ trên ) có thể đó là "vòng lặp lặp" mà anh ấy đang đề cập đến là một foreachvòng lặp, trong trường hợp cụ thể là trường hợp cụ thể của Tlà một loại giá trị, nó có thể lưu hoạt động của hộp / unbox bằng cách sử dụng vòng lặp while. Tuy nhiên, điều này không phải do IL tôi nhận được từ một phiên bản câu trả lời của bạn foreach. Tôi vẫn nghĩ rằng việc xử lý có điều kiện của iterator là quan trọng, mặc dù. Bạn có thể sửa đổi câu trả lời để bao gồm điều đó?
Josh Gallagher

5

Một vài năm sau, nhưng điều này sử dụng Linq, trả về -1 nếu không tìm thấy, không tạo thêm các đối tượng và sẽ bị đoản mạch khi tìm thấy [trái ngược với việc lặp lại trên toàn bộ 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);
}

Trong đó 'FirstOr' là:

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

Một cách khác để làm điều đó có thể là: public static int IndexOf <T> (danh sách <T>, mục T này) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( mục, x)? x + 1: -1) .Đầu tiên (x => x> 0); trả lại (e == 0)? -1: e - 1); }
Anu Thomas Chandy

"Không tạo thêm đối tượng". Linq trên thực tế sẽ tạo các đối tượng trong nền nên điều này không hoàn toàn chính xác. Cả hai source.Wheresource.DefaultIfEmptyví dụ sẽ tạo ra một IEnumerable.
Martin Odhelius

1

Tình cờ tìm thấy điều này ngày hôm nay trong một cuộc tìm kiếm câu trả lời và tôi nghĩ rằng tôi đã thêm phiên bản của mình vào danh sách (Không có ý định chơi chữ). Nó sử dụng toán tử điều kiện null của 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;

Tôi đã thực hiện một số 'cuộc đua của những con ngựa già' (thử nghiệm) và đối với các bộ sưu tập lớn (~ 100.000), trường hợp xấu nhất mà mục bạn muốn là ở cuối, điều này nhanh hơn gấp 2 lần so với thực hiện ToList().FindIndex(). Nếu Mục bạn muốn nằm ở giữa ~ 4x nhanh hơn .

Đối với các bộ sưu tập nhỏ hơn (~ 10.000), dường như chỉ nhanh hơn một chút

Đây là cách tôi đã thử nghiệm https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e


1

Sử dụng câu trả lời của @Marc Gravell, tôi đã tìm ra cách sử dụng phương pháp sau:

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

để có được -1 khi không thể tìm thấy vật phẩm:

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

Tôi đoán cách này có thể là nhanh nhất và đơn giản hơn. Tuy nhiên, tôi chưa thử nghiệm màn trình diễn.


0

Một cách khác để tìm chỉ mục sau khi thực tế là bao bọc Enumerable, hơi giống với sử dụng phương thức 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();
    }
}

Cung cấp cho một trường hợp sử dụng:

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

đường cơ sở tuyệt vời. Thật hữu ích khi thêm thành viên IsEven, IsOdd, IsFirst và IsLast.
JJS

0

Điều này có thể trở nên thực sự thú vị với một tiện ích mở rộng (hoạt động như một proxy), ví dụ:

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

Cái nào sẽ tự động gán các chỉ mục cho bộ sưu tập có thể truy cập thông qua điều này Index tính này.

Giao diện:

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

Tiện ích mở rộng tùy chỉnh (có thể hữu ích nhất để làm việc với EF và 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();
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.