แปลงรายการเป็นพจนานุกรมโดยใช้ linq และไม่ต้องกังวลกับรายการซ้ำ


163

ฉันมีรายการวัตถุบุคคล ฉันต้องการแปลงเป็นพจนานุกรมโดยที่สำคัญคือชื่อและนามสกุล (ตัดแบ่ง) และค่าคือวัตถุบุคคล

ปัญหาคือฉันมีบางคนที่ซ้ำซ้อนดังนั้นนี่จะระเบิดถ้าฉันใช้รหัสนี้:

private Dictionary<string, Person> _people = new Dictionary<string, Person>();

_people = personList.ToDictionary(
    e => e.FirstandLastName,
    StringComparer.OrdinalIgnoreCase);

ฉันรู้ว่ามันฟังดูแปลก แต่ตอนนี้ฉันไม่สนใจชื่อที่ซ้ำกันจริงๆ หากมีหลายชื่อฉันแค่อยากจะคว้ามันมา มีอยู่แล้วฉันสามารถเขียนรหัสนี้ด้านบนเพื่อใช้ชื่อเดียวและไม่ระเบิดซ้ำ


1
รายการที่ซ้ำกัน (ตามคีย์) ฉันไม่แน่ใจว่าคุณต้องการเก็บไว้หรือสูญหายหรือไม่ การรักษาพวกเขาจะทำให้จำเป็นDictionary<string, List<Person>>(หรือเทียบเท่า)
Anthony Pegram

@Anthony Pegram - เพียงแค่ต้องการให้หนึ่งในนั้น ฉันได้อัปเดตคำถามให้ชัดเจนยิ่งขึ้น
leora

คุณสามารถใช้ความแตกต่างก่อนทำ ToDictionary แต่คุณจะต้องแทนที่ Equals () และ GetHashCode () วิธีการสำหรับคลาสบุคคลเพื่อให้ CLR รู้วิธีเปรียบเทียบวัตถุบุคคล
Sujit.Warrier

@ Sujit.Warrier - คุณสามารถสร้างตัวเปรียบเทียบความเท่าเทียมกันเพื่อส่งผ่านไปยังDistinct
Kyle Delaney

คำตอบ:


71

นี่คือวิธีการแก้ปัญหาที่ชัดเจนและไม่ใช่ linq:

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}

208
นั่นคือปี 2007 :)
2010

3
ที่ไม่สนใจกรณี
on

ใช่ประมาณเวลาที่เราอัปเดตจากกรอบงาน. net 2.0 ในที่ทำงาน ... @ ไม่ยากที่จะมองข้ามตัวพิมพ์ เพียงเพิ่มคีย์ทั้งหมดเป็นตัวพิมพ์ใหญ่
Carra

ฉันจะทำให้กรณีนี้ไม่รู้สึกได้อย่างไร
leora

11
หรือสร้างพจนานุกรมด้วย StringComparer ที่จะเพิกเฉยต่อกรณีและปัญหาหากคุณต้องการรหัสการเพิ่ม / การตรวจสอบของคุณจะไม่สนใจว่าคุณกำลังเพิกเฉยกรณีหรือไม่
ไบนารี Worrier

423

วิธีการแก้ปัญหา LINQ:

// Use the first value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);

// Use the last value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);

หากคุณต้องการโซลูชันที่ไม่ใช่ LINQ คุณสามารถทำสิ่งนี้ได้:

// Use the first value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    if (!_people.ContainsKey(p.FirstandLastName))
        _people[p.FirstandLastName] = p;
}

// Use the last value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    _people[p.FirstandLastName] = p;
}

6
@LukeH หมายเหตุเล็กน้อย: ตัวอย่างสองรายการของคุณไม่เท่ากัน: ตัวแปร LINQ ยังคงเป็นองค์ประกอบแรกส่วนข้อมูลที่ไม่ใช่ LINQ จะเก็บองค์ประกอบสุดท้ายไว้หรือไม่
toong

4
@toong: นั่นเป็นเรื่องจริงและไม่ควรพลาด (แม้ว่าในกรณีนี้ OP ดูเหมือนจะไม่สนใจว่าองค์ประกอบใดที่พวกเขาจะต้องจบลงด้วย)
Luke

1
สำหรับกรณี "ค่าแรก": โซลูชัน nonLinq ทำการค้นหาพจนานุกรมสองครั้ง แต่ Linq ทำการทำอินสแตนซ์ของวัตถุที่ซ้ำซ้อนและวนซ้ำ ทั้งสองไม่เหมาะ
SerG

@SerG โชคดีที่การค้นหาพจนานุกรมถือว่าเป็นการดำเนินงาน O (1) และมีผลกระทบเล็กน้อย
MHollis

43

Linq-solution ที่ใช้ Distinct () และไม่มีการจัดกลุ่ม:

var _people = personList
    .Select(item => new { Key = item.Key, FirstAndLastName = item.FirstAndLastName })
    .Distinct()
    .ToDictionary(item => item.Key, item => item.FirstFirstAndLastName, StringComparer.OrdinalIgnoreCase);

ฉันไม่รู้ว่ามันดีกว่าโซลูชันของลุค แต่ก็ใช้ได้ดี


คุณแน่ใจเหรอ Distinct จะเปรียบเทียบประเภทอ้างอิงใหม่ที่คุณสร้างอย่างไร ฉันคิดว่าคุณต้องส่ง IEqualityComparer ไปยัง Distinct เพื่อให้ได้งานนี้ตามที่ตั้งใจไว้
Simon Gillbee

5
ละเว้นความคิดเห็นก่อนหน้าของฉัน ดูstackoverflow.com/questions/543482/…
Simon Gillbee

หากคุณต้องการลบล้างความแตกต่างที่กำหนดไว้ลองดูstackoverflow.com/questions/489258/ …
James McMahon

30

สิ่งนี้ควรใช้กับแลมบ์ดานิพจน์:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);

2
ต้องเป็น:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61

4
สิ่งนี้จะใช้ได้ก็ต่อเมื่อค่าเริ่มต้น IEqualityComparer สำหรับคลาส Person นั้นเปรียบเทียบโดยใช้ชื่อและนามสกุลโดยไม่สนใจขนาดตัวพิมพ์ มิฉะนั้นเขียนเช่น IEqualityComparer และใช้เกินพิกัดที่เกี่ยวข้อง วิธีการ ToDIctionary ของคุณควรใช้ตัวเปรียบเทียบแบบตัวพิมพ์เล็กและตัวพิมพ์
Joe

13

คุณยังสามารถใช้ToLookupฟังก์ชัน LINQ ซึ่งคุณสามารถใช้แทนกันได้เกือบกับพจนานุกรม

_people = personList
    .ToLookup(e => e.FirstandLastName, StringComparer.OrdinalIgnoreCase);
_people.ToDictionary(kl => kl.Key, kl => kl.First()); // Potentially unnecessary

สิ่งนี้จะทำโดย GroupBy ในคำตอบของลุค แต่จะให้การแปลที่พจนานุกรมให้ ดังนั้นคุณอาจไม่จำเป็นต้องแปลงเป็นพจนานุกรม แต่ใช้Firstฟังก์ชันLINQ เมื่อใดก็ตามที่คุณต้องการเข้าถึงค่าสำหรับคีย์


8

คุณสามารถสร้างวิธีการขยายคล้ายกับ ToDictionary () ด้วยความแตกต่างที่จะช่วยให้ซ้ำกัน สิ่งที่ต้องการ:

    public static Dictionary<TKey, TElement> SafeToDictionary<TSource, TKey, TElement>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector, 
        Func<TSource, TElement> elementSelector, 
        IEqualityComparer<TKey> comparer = null)
    {
        var dictionary = new Dictionary<TKey, TElement>(comparer);

        if (source == null)
        {
            return dictionary;
        }

        foreach (TSource element in source)
        {
            dictionary[keySelector(element)] = elementSelector(element);
        }

        return dictionary; 
    }

ในกรณีนี้หากมีการซ้ำซ้อนค่าสุดท้ายจะเป็นผู้ชนะ


7

ในการจัดการกับการกำจัดรายการที่ซ้ำกันให้ใช้สิ่งIEqualityComparer<Person>ที่สามารถใช้ในDistinct()วิธีการจากนั้นการทำให้พจนานุกรมของคุณง่ายขึ้น ได้รับ:

class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        return x.FirstAndLastName.Equals(y.FirstAndLastName, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode(Person obj)
    {
        return obj.FirstAndLastName.ToUpper().GetHashCode();
    }
}

class Person
{
    public string FirstAndLastName { get; set; }
}

รับพจนานุกรมของคุณ:

List<Person> people = new List<Person>()
{
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Jane Thomas" }
};

Dictionary<string, Person> dictionary =
    people.Distinct(new PersonComparer()).ToDictionary(p => p.FirstAndLastName, p => p);

2
        DataTable DT = new DataTable();
        DT.Columns.Add("first", typeof(string));
        DT.Columns.Add("second", typeof(string));

        DT.Rows.Add("ss", "test1");
        DT.Rows.Add("sss", "test2");
        DT.Rows.Add("sys", "test3");
        DT.Rows.Add("ss", "test4");
        DT.Rows.Add("ss", "test5");
        DT.Rows.Add("sts", "test6");

        var dr = DT.AsEnumerable().GroupBy(S => S.Field<string>("first")).Select(S => S.First()).
            Select(S => new KeyValuePair<string, string>(S.Field<string>("first"), S.Field<string>("second"))).
           ToDictionary(S => S.Key, T => T.Value);

        foreach (var item in dr)
        {
            Console.WriteLine(item.Key + "-" + item.Value);
        }

ฉันขอแนะนำให้คุณปรับปรุงตัวอย่างของคุณโดยอ่านตัวอย่างที่น้อยที่สุดสมบูรณ์และตรวจสอบได้
IlGala

2

ในกรณีที่เราต้องการบุคคลทั้งหมด (แทนที่จะเป็นเพียงคนเดียว) ในพจนานุกรมที่ส่งคืนเราสามารถ:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));

1
ขออภัยไม่ต้องตรวจสอบการแก้ไขของฉัน (ฉันไม่สามารถหาที่จะลบรีวิวแก้ไขของฉัน) ฉันแค่ต้องการเพิ่มข้อเสนอแนะเกี่ยวกับการใช้ g.First () แทน g.Select (x => x)
อเล็กซ์ 75

1

ปัญหากับที่สุดของคำตอบอื่น ๆ ที่พวกเขาใช้Distinct, GroupByหรือToLookupซึ่งจะสร้างพจนานุกรมพิเศษภายใต้ประทุน ToUpper เท่าเทียมกันสร้างสตริงพิเศษ นี่คือสิ่งที่ฉันทำซึ่งเกือบเป็นสำเนาที่แน่นอนของรหัส Microsoft ยกเว้นการเปลี่ยนแปลงเพียงครั้งเดียว:

    public static Dictionary<TKey, TSource> ToDictionaryIgnoreDup<TSource, TKey>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null) =>
        source.ToDictionaryIgnoreDup(keySelector, i => i, comparer);

    public static Dictionary<TKey, TElement> ToDictionaryIgnoreDup<TSource, TKey, TElement>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer = null)
    {
        if (keySelector == null)
            throw new ArgumentNullException(nameof(keySelector));
        if (elementSelector == null)
            throw new ArgumentNullException(nameof(elementSelector));
        var d = new Dictionary<TKey, TElement>(comparer ?? EqualityComparer<TKey>.Default);
        foreach (var element in source)
            d[keySelector(element)] = elementSelector(element);
        return d;
    }

เนื่องจากชุดของตัวทำดัชนีทำให้มันเพิ่มคีย์มันจะไม่โยนและจะทำการค้นหาคีย์เดียวเท่านั้น IEqualityComparerตัวอย่างเช่นคุณสามารถให้StringComparer.OrdinalIgnoreCase


0

เริ่มต้นจากโซลูชันของ Carra คุณยังสามารถเขียนเป็น:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}

3
ไม่ใช่ว่าทุกคนจะพยายามใช้สิ่งนี้ แต่อย่าพยายามใช้สิ่งนี้ การแก้ไขคอลเล็กชันในขณะที่คุณกำลังวนซ้ำเป็นความคิดที่ไม่ดี
kidmosey
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.