Rocho.J

人脑是不可靠的, 随时记录感悟并且经常重复!

 

[转]Linq的Distinct太不给力了 --- http://www.cnblogs.com/LoveJenny/archive/2011/08/01/2124233.html

假设我们有一个类:Product

public class Product
{
    public string Id { get; set; }
    public string Name { get; set; }
}

Main函数如下:

static void Main()
{
    List<Product> products = new List<Product>()
    {
        new Product(){ Id="1", Name="n1"},
        new Product(){ Id="1", Name="n2"},
        new Product(){ Id="2", Name="n1"},
        new Product(){ Id="2", Name="n2"},
    };

    var distinctProduct = products.Distinct();

    Console.ReadLine();
}

 

 

可以看到distinctProduct 的结果是:

image

 

因为Distinct 默认比较的是Product对象的引用,所以返回4条数据。

那么如果我们希望返回Id唯一的product,那么该如何做呢?

 

 

Distinct方法还有另一个重载:

//通过使用指定的 System.Collections.Generic.IEqualityComparer<T> 对值进行比较
//返回序列中的非重复元素。
 public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, 
IEqualityComparer<TSource> comparer);

该重载接收一个IEqualityComparer的参数。

假设要按Id来筛选,那么应该新建类ProductIdComparer 内容如下:

public class ProductIdComparer : IEqualityComparer<Product>
{
    public bool Equals(Product x, Product y)
    {
        if (x == null)
            return y == null;
        return x.Id == y.Id;
    }

    public int GetHashCode(Product obj)
    {
        if (obj == null)
            return 0;
        return obj.Id.GetHashCode();
    }
}

使用的时候,只需要

var distinctProduct = products.Distinct(new ProductIdComparer());

结果如下:

image

 

现在假设我们要 按照 Name来筛选重复呢?

很明显,需要再添加一个类ProductNameComparer.

那能不能使用泛型类呢??

 

新建类PropertyComparer<T> 继承IEqualityComparer<T> 内容如下:

public class PropertyComparer<T> : IEqualityComparer<T>
{
    private PropertyInfo _PropertyInfo;

    /// <summary>
    /// 通过propertyName 获取PropertyInfo对象    
    /// </summary>
    /// <param name="propertyName"></param>
    public PropertyComparer(string propertyName)
    {
        _PropertyInfo = typeof(T).GetProperty(propertyName,
        BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public);
        if (_PropertyInfo == null)
        {
            throw new ArgumentException(string.Format("{0} is not a property of type {1}.", 
                propertyName, typeof(T)));
        }
    }

    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        object xValue = _PropertyInfo.GetValue(x, null);
        object yValue = _PropertyInfo.GetValue(y, null);

        if (xValue == null)
            return yValue == null;

        return xValue.Equals(yValue);
    }

    public int GetHashCode(T obj)
    {
        object propertyValue = _PropertyInfo.GetValue(obj, null);

        if (propertyValue == null)
            return 0;
        else
            return propertyValue.GetHashCode();
    }

    #endregion
}

 

 

主要是重写的Equals 和GetHashCode 使用了属性的值比较。

使用的时候,只需要:

//var distinctProduct = products.Distinct(new PropertyComparer<Product>("Id"));
var distinctProduct = products.Distinct(new PropertyComparer<Product>("Name"));

 

结果如下:

 

image

 

为什么微软不提供PropertyEquality<T> 这个类呢?

按照上面的逻辑,这个类应该没有很复杂啊,细心的同学可以发现PropertyEquality 大量的使用了反射。每次获取属性的值的时候,都在调用
_PropertyInfo.GetValue(x, null);

可想而知,如果要筛选的记录非常多的话,那么性能无疑会受到影响。

为了提升性能,可以使用表达式树将反射调用改为委托调用

具体代码如下:

 

public class FastPropertyComparer<T> : IEqualityComparer<T>
{
    private Func<T, Object> getPropertyValueFunc = null;

    /// <summary>
    /// 通过propertyName 获取PropertyInfo对象
    /// </summary>
    /// <param name="propertyName"></param>
    public FastPropertyComparer(string propertyName)
    {
        PropertyInfo _PropertyInfo = typeof(T).GetProperty(propertyName,
        BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public);
        if (_PropertyInfo == null)
        {
            throw new ArgumentException(string.Format("{0} is not a property of type {1}.", 
                propertyName, typeof(T)));
        }

        ParameterExpression expPara = Expression.Parameter(typeof(T), "obj");
        MemberExpression me = Expression.Property(expPara, _PropertyInfo);
        getPropertyValueFunc = Expression.Lambda<Func<T, object>>(me, expPara).Compile();
    }

    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        object xValue = getPropertyValueFunc(x);
        object yValue = getPropertyValueFunc(y);

        if (xValue == null)
            return yValue == null;

        return xValue.Equals(yValue);
    }

    public int GetHashCode(T obj)
    {
        object propertyValue = getPropertyValueFunc(obj);

        if (propertyValue == null)
            return 0;
        else
            return propertyValue.GetHashCode();
    }

    #endregion
}

 

可以看到现在获取值只需要getPropertyValueFunc(obj) 就可以了。

使用的时候:

var distinctProduct = products.Distinct(new FastPropertyComparer<Product>("Id")).ToList();
 
===相关评论, 标记下:
#1楼 2011-08-01 20:17 天行健 自强不息      
为什么不用GroupBy呢,用这个简单多了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Product> products = new List<Product>()
{
    new Product(){ Id="1", Name="n1"},
    new Product(){ Id="1", Name="n2"},
    new Product(){ Id="2", Name="n1"},
    new Product(){ Id="2", Name="n2"},
};
 
var result = products.GroupBy(p => p.Id).Select(
    p=>new
    {
        Id=p.Key,
        Name=p.FirstOrDefault().Name
    });
result.ToList().ForEach(v =>
    {
        Console.WriteLine(v.Id + ":" + v.Name);
    });
 回复 引用 查看   
#2楼[楼主] 2011-08-01 20:34 LoveJenny      
@天行健 自强不息
Distinct的语意更清楚点,代码容易理解
 回复 引用 查看   
#3楼 2011-08-01 20:37 Moen      
LZ,我觉得这样写更好,可以减少反射带来的性能损失:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class DelegatedEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> r_EqualityComparer;
    readonly Func<T, int> r_GetHashCode;
  
    public DelegatedEqualityComparer(Func<T, T, bool> rpEqualityComparer, Func<T, int> rpGetHashCode)
    {
        r_EqualityComparer = rpEqualityComparer;
        r_GetHashCode = rpGetHashCode;
    }
  
    public bool Equals(T x, T y) { return r_EqualityComparer(x, y); }
    public int GetHashCode(T rpObject) { return r_GetHashCode(rpObject); }
}
  
/////////////////////////////////////////////////////////////////////
var rDelegatedEqualityComparer = new DelegatedEqualityComparer<Product>((x, y) => x == null ? y == null : x.Name == y.Name,
                                                                        r => r == null ? 0 : r.Name.GetHashCode());
                                                                                                  
var rDistinctProducts = rProducts.Distinct(rDelegatedEqualityComparer).ToArray();
 回复 引用 查看   
#4楼 2011-08-01 20:39 Yufei Huang      
你想Distinct出来什么结果?
new Product(){ Id="1" },
new Product(){ Id="1" } ??
那product的Name是什么, n1?还是n2?

如果你想要id或者Name:
products.Select(p => p.Id).Distince();
products.Select(p => p.Name).Distince();
 回复 引用 查看   

#8楼[楼主] 2011-08-01 20:45 LoveJenny      
@《YY》
难道你没看到后面使用表达式树的例子吗?
 回复 引用 查看   
#9楼 2011-08-01 20:46 《YY》      
引用LoveJenny:
@《YY》
难道你没看到后面使用表达式树的例子吗?



我看了,这个也是出于性能考虑的,

如果只是贪图方便的话 当然反射肯定好了。

各取所需吧。
 回复 引用 查看   
#10楼 2011-08-01 20:58 FJ. Zhou      
这个其实很容易解决。这种类明显是实体类,在实现时应该同时覆盖默认的Equals和GetHashCode。这样Distinct就能判断两个对象是否相等来去掉重复的。
 回复 引用 查看   
#11楼 2011-08-01 20:59 《YY》      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class KeyEqualityComparer<T> : IEqualityComparer<T>
    {
        private readonly Func<T, T, bool> comparer;
        private readonly Func<T, object> keyExtractor;
  
        public KeyEqualityComparer(Func<T, object> keyExtractor) :
            this(keyExtractor, null) { }
  
        public KeyEqualityComparer(Func<T, T, bool> comparer) :
            this(null, comparer) { }
  
        public KeyEqualityComparer(Func<T, object> keyExtractor, Func<T, T, bool> comparer)
        {
            this.keyExtractor = keyExtractor; this.comparer = comparer;
        }
  
        public bool Equals(T x, T y)
        {
            if (comparer != null)
                return comparer(x, y);
            else
            {
                var valX = keyExtractor(x); if (valX is IEnumerable<object>)
  
                    return ((IEnumerable<object>)valX).SequenceEqual((IEnumerable<object>)keyExtractor(y));
                return valX.Equals(keyExtractor(y));
            }
        }
        public int GetHashCode(T obj)
        {
            if (keyExtractor == null)
                return obj.ToString().ToLower().GetHashCode();
            else
            {
                var val = keyExtractor(obj);
                if (val is IEnumerable<object>)
                    return (int)((IEnumerable<object>)val).Aggregate((x, y) => x.GetHashCode() ^ y.GetHashCode()); return val.GetHashCode();
            }
        }
    }
//////////////////////////////
var distinctProduct = products.Distinct(new KeyEqualityComparer<Product>(c => c.Id))
 回复 引用 查看   
#12楼 2011-08-01 21:01 鹤冲天      
用扩展方法解决,非常简单:
var p1 = products.Distinct(p => p.ID)

请参见我的文章:c#扩展方法奇思妙用基础篇八:Distinct 扩展
 回复 引用 查看   
#13楼 2011-08-01 22:11 _龙猫      
期待Distinct来完成原本不属于它完成的功能(GroupBy),当然会说不给力了。
 回复 引用 查看   
#14楼 2011-08-01 23:55 Jeffrey Zhao      
API编写和设计能力不过关啊……
 回复 引用 查看   
#15楼 2011-08-02 08:51 木鱼      
个人在这种情况下倾向于让 Product 自己实现 IEquatable 接口。
 回复 引用 查看   
#16楼 2011-08-02 09:36 越狱的鱼      
楼上童鞋们,先不要说API不给力。。。
先看看Distinct用法
var distinctProduct = products.Distinct();
对应生成的sql语句是啥?
select distinct Id,Name from products 结果会是2条?

Distinct 没这么用的吧。。。
 回复 引用 查看   
#17楼 2011-08-02 09:41 刀 刀      
这是我认为目前C#唯一不如Java的一个地方,就是C#里不能创建一个实现了某接口的匿名类,Java在这一点上还是比较方便的。C#的LINQ扩展方法中很多地方用到接受IEqualityComparer<T>参数,有的时候为了一次调用,就要写一个comparer。写个通用的又不可避免反射/dynamic/表达式树(不是不可以,就是这一点没有Java舒服)
 回复 引用 查看   
#18楼 2011-08-02 09:43 刀 刀      
@越狱的鱼
引用越狱的鱼:
楼上童鞋们,先不要说API不给力。。。
先看看Distinct用法
var distinctProduct = products.Distinct();
对应生成的sql语句是啥?
select distinct Id,Name from products 结果会是2条?
Distinct 没这么用的吧。。。

谁告诉你这里Distinct会被解析成sql语句了……
 回复 引用 查看   
#19楼 2011-08-02 09:59 越狱的鱼      
引用刀 刀:
@越狱的鱼
引用越狱的鱼:
楼上童鞋们,先不要说API不给力。。。
先看看Distinct用法
var distinctProduct = products.Distinct();
对应生成的sql语句是啥?
select distinct Id,Name from products 结果会是2条?
Distinct 没这么用的吧。。。

谁告诉你这里Distinct会被解析成sql语句了……


没被被解析成sql语句,为了容易理解,举个例子 :)
 回复 引用 查看   
#20楼 2011-08-02 10:05 田想兵      
引用越狱的鱼:
引用刀 刀:
@越狱的鱼
引用越狱的鱼:
楼上童鞋们,先不要说API不给力。。。
先看看Distinct用法
var distinctProduct = products.Distinct();
对应生成的sql语句是啥?
select distinct Id,Name from products 结果会是2条?
Distinct 没这么用的吧。。。

谁告诉你这里Distinct会被解析成sql语句了……


没被被解析成sql语句,为了容易理解,举个例子 :)

弱弱的問一句,Distinct能不能求和?哈哈~~~~~
 回复 引用 查看   
#21楼 2011-08-02 10:09 越狱的鱼      
引用田想兵:
引用越狱的鱼:
引用刀 刀:
@越狱的鱼
引用越狱的鱼:
楼上童鞋们,先不要说API不给力。。。
先看看Distinct用法
var distinctProduct = products.Distinct();
对应生成的sql语句是啥?
select distinct Id,Name from products 结果会是2条?
Distinct 没这么用的吧。。。

谁告诉你这里Distinct会被解析成sql语句了……


没被被解析成sql语句,为了容易理解,举个例子 :)

弱弱的問一句,Distinct...


我只能说Distinct太不给力了!
 回复 引用 查看   
#22楼 2011-08-02 12:11 testzhangsan      
mark
 回复 引用 查看   
#23楼 2011-08-02 12:44 鹤冲天      
引用刀 刀:这是我认为目前C#唯一不如Java的一个地方,就是C#里不能创建一个实现了某接口的匿名类,Java在这一点上还是比较方便的。C#的LINQ扩展方法中很多地方用到接受IEqualityComparer<T>参数,有的时候为了一次调用,就要写一个comparer。写个通用的又不可避免反射/dynamic/表达式树(不是不可以,就是这一点没有Java舒服)

c# 中有嵌套类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
  
    public IEqualityComparer<Product> GetIDComparer()
    {
        return new IDComparer();
    }
  
    class IDComparer : IEqualityComparer<Product>
    {
        public bool Equals(Product x, Product y)
        {
            return EqualityComparer<int>.Default.Equals(x.ID, y.ID);
        }
  
        public int GetHashCode(Product obj)
        {
            return EqualityComparer<int>.Default.GetHashCode(obj.ID);
        }
    }
}

用起来不比 Java 的匿名类差
 回复 引用 查看   
#24楼 2011-08-02 12:54 刀 刀      
@鹤冲天
我没说不能做啊,我是说,为了做某次查询,要专门写个类,这个comparer就为了这一个地方用一次。如果可以匿名的话,就很方便。
1
2
3
4
products.Distinct(new IEqualityComparer<Product>() {
   public int GetHashCode() { ... }
   public bool Equals(Product p) { ... }
});

不用为了这一次查询去写个comparer类,而且,下次如果逻辑发生修改,不用还定位到Product类那儿去修改逻辑。
 回复 引用 查看   
#25楼 2011-08-02 14:55 Ivony...      
典型的SQL的思维,,,,,

这就是个GroupBy的问题。SQL的DISTINCT才是行为诡异的的,没有确定的结果。
 回复 引用 查看   
#26楼 2011-08-02 21:23 鹤冲天      
引用刀 刀:
@鹤冲天
我没说不能做啊,我是说,为了做某次查询,要专门写个类,这个comparer就为了这一个地方用一次。如果可以匿名的话,就很方便。
1
2
3
4
products.Distinct(new IEqualityComparer<Product>() {
   public int GetHashCode() { ... }
   public bool Equals(Product p) { ... }
});

不用为了这一次查询去写个comparer类,而且,下次如果逻辑发生修改,不用还定位到Product类那儿去修改逻辑。

看看我的新随笔:何止 Linq 的 Distinct 不给力
里面有更简单的创建 IEqualityComparer<Product> 实例的方法。

posted on 2012-03-08 15:02  RJ  阅读(1064)  评论(0编辑  收藏  举报

导航