10 Aralık 2013 Salı

Distinct İslemi Yapmanin 4 Yolu


Merhaba Arkadaşlar,

Yazılım geliştirirken zaman zaman eldeki koleksiyon tipindeki veriler üzerinde kolon(lar) bazında distinct(farklı, ayrı) işlemiyle benzer kayıtları filtrelememiz gerekebilir. Sql tarafında bu işlem için "distinct" komutu vardır; .net tarafında Enumerable sınıfı içerisindeki Distinct() genişletme metoduyla veya çeşitli linq sorgularıyla bu işlem kotarılabilir. İşin özünde nesnelerin farklılığını saptama olduğundan, eldeki çözümler artalanda nesnelerin kimlik niteliği olan hasCode'a dayanır. Tüm sınıflar örtülü olarak Object sınıfından kalıtıldıkları için de her nesnenin Object sınıfı üzerinden gelen GetHashCode() metoduyla nesnenin hash koduna ulaşılıp eş(it)lik kontrolü yapılabilir.


Örneğin elimizde aşağıdaki gibi bir Person sınıfı ve koleksiyonu olsun:
public class Person
{
 public int Id { get; set; }

 public string Name { get; set; }
}

var people = new[]
  {
   new Person{ Id=1, Name = "Ali" },
   new Person{ Id=2, Name = "Veli" },
   new Person{ Id=3, Name = "Cem" },
   new Person{ Id=2, Name = "Veli" },
   new Person{ Id=1, Name = "Ali" }
  };
Ve biz de bu koleksiyona Id ve Name property'lerine göre distinct işlemi uygularsak sonuç aşağıdaki gibi olmalıdır:
var filteredPeople = new[]
  {
   new Person{ Id=1, Name = "Ali" },
   new Person{ Id=2, Name = "Veli" },
   new Person{ Id=3, Name = "Cem" },
  };
Ben nette araştırıp, biraz da kendim bir şeyler katarak 4 farklı yöntem bulabildim, belki siz daha fazlasını bulur ya da akıl edebilirsiniz :] Gelin bu yöntemleri tek tek inceleyelim:

1. Linq Sorgusunda GroupBy Kullanarak
Koleksiyonu istenen kolonlara göre gruplayıp, oluşan grupların ilk üyesini seçerek. En sade ve anlaşılır çözüm buydu benim için. ( "bana bu yeter hacı" diyenler, sonraki çözümlere bakmayıp linq sorgusunu kopyalamak suretiyle sekmeyi kapayabilirler :] )
var res1 = people.GroupBy(a => new { a.Id, a.Name }).Select(a => a.First());
2. Linq Sorgusunda Select ve Distinct Birlikte Kullanılarak
Select ifadesi ile belirtilen property'lere göre distinct işlemi uygulanır.
var res2 = people.Select(a => new Person{ Id=a.Id, Name=a.Name }).Distinct();
3. Enumerable.Distinct() Genişletme Metodunu Kullanarak
Bu genişletme metodunun 2 overload'ı vardır; ilki parametresiz olanıdır ki, işlem yapılacak tipin IEquatable interface'ini implemente etmiş olmasını bekler, distinct işlemi için gereken mantık burdan sağlanır, diğeri de parametre olarak IEqualityComparer interfacesini implemente eden bir sınıf örneğini alır. O zaman ilk iş Person sınıfının IEquatable arayüzünü uygulamasıdır:

public class Person : IEquatable<Person>
{
 public int Id { get; set; }

 public string Name { get; set; }

 public bool Equals(Person other)
 {
  return Id.Equals(other.Id) && Name.Equals(other.Name);
 }

 public override int GetHashCode()
 {
  return Id.GetHashCode() * Name.GetHashCode();
 }
}
IEquatable arayüzü Equals() ve GetHasCode() metodlarını ezmemizi şart koştu, Name ve Id kolonlarına göre distinct işlemi yapmak istediğimiz için bu şekilde yazdım metodları. Bu interface sayesinde distinct metodu neye göre işlem yapacağını bilir yoksa işleme sokulan koleksiyonun aynısını döndürür. Kullanımı:
var people = // kaynaktan veriyi çek
var res3 = people.Distinct();
4. Enumerable.Distinct() Genişletme Metodununun IEqualityComparer Tipini Paremetre Olarak Alan Overload'unu Kullanarak
IEqualityComparer arayüzü koleksiyon tipi için implemente edilir, Distinct() metoduna iş mantığı burdan aktarılır:
public class PersonComparer : IEqualityComparer<Person>
{
 public bool Equals(Person x, Person y)
 {
  return x.Id.Equals(y.Id) && x.Name.Equals(y.Name);
 }

 public int GetHashCode(Person obj)
 {
  return obj.Id.GetHashCode() * obj.Name.GetHashCode();
 }
}
Bir önceki yöntemle aynı mantık uygulanır fakat direk ilgili tip üzerinden uygulanmayarak esneklik sağlar. Kullanımı:
var people = // kaynaktan veriyi çek
var res4 = people.Distinct(new PersonComparer());
4 yöntemi bir araya getirip, test edersek:
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqDistinctProblem
{
    class Program
    {
        static void Main(string[] args)
        {
            var people = new[]
            {
                new Person{ Id=1, Name = "Ali" },
                new Person{ Id=2, Name = "Veli" },
                new Person{ Id=3, Name = "Cem" },
                new Person{ Id=2, Name = "Veli" },
                new Person{ Id=1, Name = "Ali" }
            };

            var res1 = people.GroupBy(a => new { a.Id, a.Name }).Select(a => a.First());
            display("groupBy", res1);

            var res2 = people.Select(a => new Person{ Id=a.Id, Name=a.Name }).Distinct();
            display("select", res2);

            var res3 = people.Distinct();
            display("implementing IEquatable", res3);

            var res4 = people.Distinct(new PersonComparer());
            display("implementing IEqualityComparer", res4);
        }

        private static void display(string method, IEnumerable result)
        {
            Console.WriteLine("\n> "+method);
            foreach (var item in result) Console.WriteLine(item.Id + " " + item.Name);
        }
    }
}





















Ekran çıktısından da gördüğümüz üzere amacımıza ulaştık.

Son yöntemde kullanılan IEqualityComparer tipinin reflection ve generic kullanılarak genelleştirilmiş versiyonunu şurada bulabilirsiniz. Hatta IEqualityComparer'in karşılaştırma mantığını lambda ( delegate(T, T) => bool ) olarak alan ve de çok kullanışlı olan sarmalayıcı sınıfı da şuradan ulaşabilirsiniz.

Distinct işlemi üzerine yazacaklarım şimdilik bu kadar. Şaka şaka :] Sıkılmadan buraya kadar okuduysanız teşekkürler, bir sonraki blog girdisinde görüşmek üzere :]

Faydalanılan Kaynaklar:
https://code.google.com/p/morelinq/
http://stackoverflow.com/questions/1300088/distinct-with-lambda
http://msdn.microsoft.com/tr-tr/library/bb348436(v=vs.110).aspx




Hiç yorum yok:

Yorum Gönder