引爆你的集合灵感 [C#, LINQ]

引爆你的集合灵感 [C#, LINQ]

SET FORTH YOUR SET IDEARS [C#, LINQ]

 

WRITTEN BY ALLEN LEE

 

0. TABLE OF CONTENT

  • 1. WHAT ARE THE DIFFERENCES?
  • 2. USING SET<T> COLLECTION OF POWERCOLLECTION.
  • 3. USING SET OPERATORS OF LINQ.
  • 4. SET<T> COLLECTION VS. SET OPERATORS.
  • 5. FURTHER CONSIDERATIONS.
  • 6. EXERCISES.

 

1. WHAT ARE THE DIFFERENCES?

某天,我的朋友 Heng 问我有什么工具可以比较两个文件夹,并找出其中不同的文件。首先,我们来看看他的要求:

  • 1) 仅需比较当前文件夹中的文件;
  • 2) 两个文件夹中相同名字的文件看作相同的文件。

很明显,如果把这两个文件夹可以看作两个集合,

  • A = { a | 第一个文件夹中的文件 }
  • B = { b | 第二个文件夹中的文件 }

那么他所找的文件将是对这两个集合进行一系列的集合操作所得的结果集。请看下图:

Venn diagram

红、黄两个圆分别代表 A 和 B 两个集合,其中1、2两个区域就是我们要找的“不同的文件”的集合了。使用集合表示法,这个结果集可以表示为(“'”为补集符号,全集 I = A ∪ B):

R = (A ∩ B)'

然而,这个结果集有一个盲点,就是不加区分的把1、2两个区域混合起来了,如果我们只需要其中一个区域的结果集呢?或者,我们需要把1、2两个区域分别显示到 GUI 的两个编辑框呢?

如果我们要以 A 作为基准,找出 B 中 A 没有的元素,那么结果集(区域2)将表示为:

R2 = A'

如果我们要以 B 作为基准,找出 A 中 B 没有的元素,那么结果集(区域1)将表示为:

R1 = B'

很明显,这一“方向”因素在具体编码的时候是应该加以考虑的。

搞了那么多理论,应该来点实际的了,首先,我们来看看 Set Collection 是如何使用的。

 

2. USING SET<T> COLLECTION OF POWERCOLLECTION.

首先,我们要获取指定路径下的所有文件,由于 Heng 只要求当前文件夹里的所有文件,于是:

// Code #01

static string[] GetFileNamesFrom(string path)
{
    
if (path == null || path.Length == 0)
    
{
        
throw new ArgumentException("Parameter cannot be null or empty!");
    }


    
if (!Directory.Exists(path))
    
{
        
throw new ArgumentException(String.Format("{0} does not exist!", path));
    }


    
string[] fileNames = System.IO.Directory.GetFiles(path);
    
string[] result = new string[fileNames.Length];
    
for (int i = 0; i < fileNames.Length; i++)
    
{
        result[i] 
= System.IO.Path.GetFileName(fileNames[i]);
    }


    
return result;
}

从 Heng 的要求中,我们可以看出只需要考虑文件名,所以我们把路径过滤掉了。

接着,我们创建 A 和 B 两个集合:

// Code #02

Set
<string> A = new Set<string>(GetFileNamesFrom(args[0]));
Set
<string> B = new Set<string>(GetFileNamesFrom(args[1]));

对应上面的集合讨论,我们可以这样获取结果集:

// Code #03

Set
<string> R1 = A.Difference(B);
Set
<string> R2 = B.Difference(A);

我们知道 R 是 R1 和 R2 的并集,如果你需要 R,你不需要先找出 R1 和 R2 再进行合并,而是直接使用 Set 提供的:

// Code #04

Set
<string> R = A.SymmetricDifference(B);

// OR

Set
<string> R = B.SymmetricDifference(A);

根据集合的性质,这两种做法是等效的。

现在,我们来看看 LINQ 中的 Set Operators 又是如何操作的。

 

3. USING SET OPERATORS OF LINQ.

有了 LINQ,你可以直接“查询”指定路径下的文件名(当然,你要保证路径的正确性):

// Code #05

var A 
= from a in System.IO.Directory.GetFiles(args[0])
        orderby a 
        select System.IO.Path.GetFileName(a);
var B 
= from b in System.IO.Directory.GetFiles(args[1])
        orderby b
        select System.IO.Path.GetFileName(b);

接着,使用 Standard Query Operators 的 Except 来获取结果集:

// Code #06

var R1 
= A.Except(B);
var R2 
= B.Except(A);

由于 LINQ 的集合操作符(Set operators)没有提供类似于 Set.SymmetricDifference 的功能,于是,如果你需要 R 的话,你可以:

// Code #07

var R 
= R1.Union(R2);

另外,你可以对 System.IO.Directory.GetFiles 进行一番包装,在你的方法内检查传入的路径参数。

 

4. SET<T> COLLECTION VS. SET OPERATORS.

由于剧情发展的需要,它们俩难免会有这样一个碰面的情节。你会选择哪个?这样一个情节、这样一个问题,使人不禁感到像在选择一个将陪你度过下半辈子的另一半,呵呵~~~

就我个人而言,我喜欢 Set Collection 的成熟,但又忘不了 Set Operators with Query 的直率。贪心的我自然希望能够坐享齐人之福,集两家之长啦。

Martin Hotel 的 Sales & Marketing 主管 Becky 发现最近的酒店入住率下降了,她决定对此进行一番调查。

Becky 深知留住一个老客户的成本要比开发一个新客户的成本低得多,于是她决定看看最近一个季度的老客户入住率是否发生变化?如何变化?

// Code #08

var q 
= from c in customers
        where c.Level 
== CustomerLevel.VIP
        group c by c.CheckInDate.Month into g
        select 
new { Month = g.Key, Count = g.Group.Count() };

Becky 发现以月为单位,老客户的入住率明显下降了,是什么原因呢?由于这些老客户都是大客户,他们只会入住五星级酒店,而本地的五星级酒店除了 Martin Hotel,就是剩下那家死对头了。Becky 发现最近有一些人经常停留在本酒店门口,她怀疑对手派人来本酒店门口拉客,于是,她找人调查那些在本酒店门口跟这些人接触后没有入住本酒店的老客户,她得到一份名单,上面列出在本酒店门口“失踪”却入住了对头酒店的老客户名字。

她把那些流失的老客户名单和已入住的老客户的名单加总:

// Code #09

// GetLostVipList() returns an IEnumerable
var l = GetLostVipList();
var g 
= from c in customers
        where c.Level 
== CustomerLevel.VIP
        select c.Name;
var t 
= g.Union(l);

// GetVipList() returns a Set
Set<Customer> vip = GetVipList();
Set
<Customer> total = new Set<T>(t);

Console.WriteLine(vip.IsEqualTo(total));

现在,Becky 终于知道老客户流失的原因了,她决定起诉对头酒店使用不正当竞争手段。

 

5. FURTHER CONSIDERATIONS.

我们知道,LINQ 目前还只是一个原型(prototype),而 PowerCollection 已经 release 了。然而,你不必为 LINQ 仅提供4个集合操作而烦恼,由于 LINQ 支持 Extension Methods,你可以根据需要自行扩展集合操作:

// Code #10

namespace Becky.Utils
{
    
public static bool IsEqualTo<T>(this IEnumerable<T> first, IEnumerable<T> second)
    
{
        
// Add some code here
    }

}

另外,由于集合的操作必然涉及到元素的判等,于是我们有必要对于这些工具的判等方式有一个了解。Set Collection使用 System.Collection.Generic.IEqualityComparer,而 Set Operators 则使用 Equals 和 GetHashCode 两个方法。如果你放入集合中的是自定义对象,那么你就要考虑这些问题了。

补充阅读:

Object Equality and Identity in Chapter 6: Common Object Operations. Jeffrey Richter. Applied Microsoft .NET Framework Programming. Microsoft Press, 2002

最后,由于本文把重点放在集合的操作以及工具的使用上,其他一些在实际的项目中必须考虑的因素已酌情省略了。例如实际进行文件对比时,相同名字的文件的大小也是一个需要考虑的因素。这样,你首先需要定义何谓不同的文件,提取必须考虑的因素,然后抽象出一个文件的表示,最后把“查询”的结果投射(project)到该抽象中。

 

6. EXERCISES.

Heng 现在增加多一点要求,就是要考虑文件的大小,仅当文件名和文件大小相同才看作相同的文件。那么,你认为 Set Collection 和 Set Operators 两个方案应该如何修改才能满足新的需求呢?另外,如果 Heng 过几天又可能提出新的要求,你认为你应该如何设计才能更有弹性的满足这种需求的变化呢?

posted @ 2005-09-27 08:37 Allen Lee 阅读(4824) 评论(9)  编辑 收藏 所属分类: C#

  回复  引用  查看    
#1楼 2005-09-27 09:15 | 怀沙      
好文,再深入点就更好了:)
  回复  引用  查看    
#2楼 2005-09-27 09:24 | 香蕉      
如果范型T是引用类型会怎么样,做集合操作时,对元素是内容比较,还是引用比较。
  回复  引用    
#3楼 2005-09-27 09:25 | progame [未注册用户]
楼主写技术文章真是一流
  回复  引用  查看    
#4楼 2005-09-27 09:47 | linkcd      
小小补充一下,2.0下面判断path参数是否为空或为empty, 有个新的现成方法了:

//1.1
if (path == null || path.Length == 0)
{
//throw exp
}

//2.0
if(string.IsNullOrEmpty(path))
{
//throw exp
}

source code是这个
public static bool IsNullOrEmpty(string value)
{
if (value != null)
{
return (value.Length == 0);
}
return true;
}
看上去效率还是不差的

  回复  引用    
#5楼 2005-09-27 10:02 | idior [未注册用户]
public static bool IsNullOrEmpty(string value)
{
if (value != null)
{
return (value.Length == 0);
}
return true;
}


if (path == null || path.Length == 0)
{
//throw exp
}

这两段代码完全一样, 估计是reflector的问题.
  回复  引用  查看    
#6楼 2005-09-27 11:28 | FantasySoft      
R = ((A ∪ B) ∩ (A ∩ B))' 实际上等同于 (A ∩ B)'
因为(A ∩ B) ⊂ (A ∪ B).


  回复  引用  查看    
#7楼 [楼主]2005-09-27 12:06 | Allen Lee      
To FantasySoft:

你说的没错,除此之外:

R1 = B'
R2 = A'

汗~~~我还真的简单复杂化哟,呵呵~~~
  回复  引用  查看    
#8楼 [楼主]2005-09-27 12:28 | Allen Lee      

To 香蕉:

默认情况下,Set<T> 会比较对象的引用而不是内容。如果你希望 Set<T> 比较对象的内容,我这里可以提供两种方法。首先我们假设 Customer 类的代码如下:

// Code #01

class Customer
{
    
public Customer(int id, string name)
    
{
        m_ID 
= id;
        m_Name 
= name;
    }


    
private int m_ID;
    
public int ID
    
{
        
get return m_ID; }
    }


    
private string m_Name;
    
public string Name
    
{
        
get return m_Name; }
    }


    
public override string ToString()
    
{
        
return String.Format("{0} [{1}]", m_Name, m_ID);
    }

}

第一种方法是相 Set<T> 提供一个 IEqualityComparer<T> 的实现:

// Code #02

class CustomerEqualityComparer : EqualityComparer<Customer>
{
    
public override bool Equals(Customer x, Customer y)
    
{
        
if ((x == null|| (x == null))
        
{
            
return false;
        }


        
if ((x.ID == y.ID) && (x.Name == y.Name))
        
{
            
return true;
        }

        
else
        
{
            
return false;
        }

    }


    
public override int GetHashCode(Customer obj)
    
{
        
return obj.ID;
    }

}

这样,你无需修改 Customer 类就可以得到期望的输出了:

// Code #03

static void Test()
{
    Customer c1 
= new Customer(1412"Allen Lee");
    Customer c2 
= new Customer(1412"Allen Lee");
    Set
<Customer> set = new Set<Customer>(new CustomerEqualityComparer());
    
set.Add(c1);
    
set.Add(c2);
    
foreach (Customer c in set)
    
{
        Console.WriteLine(c);
    }

}


// Output:

// Allen Lee [1412]

第二种方法是在 Customer 类中重载 Equals 和 GetHashCode 两个方法:

// Code #04

public override bool Equals(object obj)
{
    
if (obj == null)
    
{
        
return false;
    }


    
if (obj.GetType() != typeof(Customer))
    
{
        
return false;
    }


    Customer that 
= (Customer)obj;

    
if ((this.m_ID == that.m_ID) && (this.m_Name == that.m_Name))
    
{
        
return true;
    }

    
else
    
{
        
return false;
    }

}


public override int GetHashCode()
{
    
return m_ID;
}

这样,你无需修改客户端代码就可以得到期望的输出了。注意,这里的“无需修改客户端代码”是指客户端无需知道并使用额外的 CustomerEqualityComparer。当然,如果你选择这种方法,你还应该为 Customer 类重载 == 和 != 两个操作符。

关于对象的判等,Jeffrey Richter 在他的 Applied Microsoft .NET Framework Programming 上有了一整篇文章详细论述了,你可以参考一下。


  回复  引用    
#9楼 2005-09-27 17:31 | 商务之窗 [未注册用户]
收藏,慢慢看

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2006-09-05 18:54 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: