接口第二部分——实现 IComparable 和 IComparer






4.74/5 (14投票s)
精心设计的示例的第二部分,演示了实现各种接口的有用性。
引言
这是两部分中的第二部分。如果您还没有阅读第一部分,强烈建议您阅读。第一部分主要关注 IEnumerable
和 IEnumerator
,而第二部分主要关注 IComparable
和 IComparer
。本文旨在承接第一部分,并假定您已熟悉该项目。
使用代码
这些代码块的布局不反映它们在源代码中的表示方式,而是按照我处理每个问题的方式进行布局。
Example6.cs
我真正讨厌的是杂乱的水果篮。在内部,我知道 FruitBasket
将每个 Fruit
存储在一个数组中。有了正确的接口,我们就应该能够对我们的篮子进行排序。
在 FruitBasket
中,添加一个新的方法 Sort
。
public void Sort()
{
Array.Sort( basket );
}
然后,在添加完所有水果之后,但在循环遍历篮子恢复名称之前,在 Main
中调用此方法。我特意将水果的添加做得不正确,以便稍后暴露一个 bug。
static void Main(string[] args)
{
FruitBasket fruitBasket = new FruitBasket();
Console.WriteLine( "Adding a Banana" );
fruitBasket.Add( new Banana() );
Console.WriteLine( "Adding an Apple" );
fruitBasket.Add( new Apple() );
// Console.WriteLine( "Adding a Cantaloupe" );
// fruitBasket.Add( new Cantaloupe() );
Console.WriteLine( "" );
Console.WriteLine( "Sorting" );
fruitBasket.Sort();
Console.WriteLine( "" );
Console.WriteLine( "The basket is holding:" );
foreach( Fruit fruit in fruitBasket )
{
Console.WriteLine( " a(n) " + fruit.Name );
}
}
如果我们编译并执行,我们会因异常崩溃,这可能不会让人感到惊讶。
An unhandled exception of type 'System.InvalidOperationException' occurred
in mscorlib.dll
Additional information: Specified IComparer threw an exception.
有趣的是,Array.Sort
使用 IComparer
接口。因为我们有一个类数组,而不是一个字符串的一维数组,所以 Array.Sort
的默认行为失败了。返回的错误具有误导性。IComparer
功能强大,并且具有一些高级功能。我怀疑内部的某个 IComparable
调用包装了一个 IComparer
调用,而这才是真正失败的地方,但就我们目前的需求而言,我们实际上需要一个 IComparable
接口。
Example7.cs
因为我们从 Fruit 派生了 Apple、Banana 和 Cantaloupe,所以我们为自己节省了一些工作。仅在 Fruit
中实现 IComparable
,我们的派生类就可以免费获得此功能。
public class Fruit : IComparable
按下 Tab 键会创建我们仍然需要编写的 IComparable
成员 CompareTo
。
#region IComparable Members
public int CompareTo(object obj)
{
// TODO: Add Fruit.CompareTo implementation
return 0;
}
#endregion
MSDN 文档告诉我们 CompareTo
“将当前实例与同一类型的另一个对象进行比较。”起初这听起来比实际情况更难。我们得到一个 object obj
,并被要求将其与当前实例进行比较。为此,我们将其重铸为 Fruit
。您可以使用 Fruit fruit = obj as Fruit;
并检查该值为 null
以防重铸失败,或者直接重铸 Fruit fruit = (Fruit) obj;
,如果失败则抛出异常。在本文中,抛出异常似乎是最明智的做法,但这取决于具体情况。
public int CompareTo(object obj)
{
Fruit fruit = (Fruit) obj;
return( this.Name.CompareTo( fruit.Name ));
}
因为我们将 Name
成员定义为 string
,所以我们可以利用这种情况,并使用我们从该类型继承的字符串的 CompareTo
方法。
编译并运行,尽管我们先添加了 Banana
然后是 Apple
,但结果正确排序并显示为 Apple
和 Banana
。现在,通过取消注释 Main
中的两行来添加 Cantaloupe
。再次编译并运行,您会发现我们崩溃了。
An unhandled exception of type 'System.NullReferenceException' occurred
in Example7.exe
Additional information: Object reference not set to an instance of an object.
这是否意味着我们的篮子不能容纳超过两种不同的水果?如果您从异常处理程序中跳出代码,您会看到我们在尝试写入第一个名称时崩溃了。进一步检查发现,篮子数组的第一个元素是未定义值。这就是我们崩溃的原因,我们当前正在尝试评估的水果是 null
;Array.Sort
会对所有内容进行排序,包括未分配的元素。更糟糕的是,null
会被排在某物之前。就我个人而言,我不理解为什么框架要这样设计,但幸运的是,这很容易修复。Sort
有大约八种不同的重载;第六种带有开始 index
和 length
参数。我们知道我们的 index
从 0 开始,而且我们一直通过 count
来跟踪我们向数组添加了多少个元素。
public void Sort()
{
Array.Sort( basket, 0, count );
}
稍作修改,我们就再也不受限于容量为二的次幂的水果篮了。
Example8.cs
所以您可以通过数组进行迭代,并按水果名称排序,但如果您想按其他指标(如质量或颜色)排序,该怎么办?这时 IComparer
就派上用场了。
public class Fruit : IComparable
{
public enum SortMetric
{
Name,
Mass,
Color
};
...
public virtual float Mass
{
get
{
return( float.NaN );
}
}
public virtual string Color
{
get
{
return( null );
}
}
...
public class Apple : Fruit
{
...
public override float Mass
{
get
{
return( 119.0f );
}
}
public override string Color
{
get
{
return( "Red" );
}
}
}
public class Banana : Fruit
{
...
public override float Mass
{
get
{
return( 92.0f );
}
}
public override string Color
{
get
{
return( "Yellow" );
}
}
}
public class Cantaloupe : Fruit
{
...
public override float Mass
{
get
{
return( 380.0f );
}
}
public override string Color
{
get
{
return( "Brown" );
}
}
}
为简洁起见,我仅列出了新增内容,并试图给出这些新成员去向的背景。省略号不是代码的一部分,而是表示某些内容已被省略。
与 IEnumerable
和 IEnumerator
类似,我们将 IComparable
和 IComparer
定义在不同的类中,但在这种情况下,IComparer
类嵌套在 Fruit
中。您稍后会看到这如何使我们更容易,但这也是出于组织方面的考虑。按 Mass
和 Color
排序仅在 Fruit
的上下文中才有意义。
#region IComparable Members
public int CompareTo(object obj)
{
Fruit fruit = (Fruit) obj;
return( this.Name.CompareTo( fruit.Name ));
}
#endregion
class SortByMassClass : IComparer
{
#region IComparer Members
public int Compare(object x, object y)
{
// TODO: Add SortByMassClass.Compare implementation
return 0;
}
#endregion
}
class SortByColorClass : IComparer
{
#region IComparer Members
public int Compare(object x, object y)
{
// TODO: Add SortByColorClass.Compare implementation
return 0;
}
#endregion
}
}
在已经实现了 CompareTo
的情况下,Compare
就没有那么困难了。正如您所能想象的,Compare
“比较两个对象并返回一个指示一个对象是否小于、等于或大于另一个对象的数值。”与我们只重铸一个参数的 CompareTo
不同,现在我们重铸两个。
public class SortByMassClass : IComparer
{
#region IComparer Members
public int Compare(object x, object y)
{
Fruit fruitx = (Fruit) x;
Fruit fruity = (Fruit) y;
return( ((IComparable) fruitx.Mass).CompareTo( fruity.Mass ));
}
#endregion
}
public class SortByColorClass : IComparer
{
#region IComparer Members
public int Compare(object x, object y)
{
Fruit fruitx = (Fruit) x;
Fruit fruity = (Fruit) y;
return( String.Compare( fruitx.Color, fruity.Color ));
}
#endregion
}
在 SortByMassClass
中,最有趣的部分是返回值。为了让 fruitx
实现 CompareTo
方法,它被重铸为 IComparable
。它还被一对额外的括号包围,以将该重铸的范围仅限于 fruitx
。然后,我们允许默认的 float
CompareTo
方法执行评估。
为了能够通过这些不同的方法进行排序,我们重载了 Sort
方法,使其能够接收某种排序依据。
public void Sort( Fruit.SortMetric sortMetric )
{
switch( sortMetric )
{
case Fruit.SortMetric.Name:
Array.Sort( basket, 0, count );
break;
case Fruit.SortMetric.Mass:
Array.Sort( basket, 0, count,
(IComparer) new Fruit.SortByMassClass());
break;
case Fruit.SortMetric.Color:
Array.Sort( basket, 0, count,
(IComparer) new Fruit.SortByColorClass());
break;
}
}
Array.Sort
有几个重载,它们期望一个 IComparer
类来处理排序。回想一下 Example6.cs 中的崩溃。Array.Sort
在不带 IComparer
类的情况下调用时,会提供一个默认的类。这就是为什么当它无法排序时,我们会收到一个关于 IComparer
的模糊错误消息。通过提供我们自己的 IComparer
类,我们现在可以按任何我们能想象到的属性进行排序和评估。
现在,按 Mass
或 Color
排序就像调用 Sort
并传入适当的 enum
一样简单。
Console.WriteLine( "" );
Console.WriteLine( "Sorting by Mass" );
fruitBasket.Sort( Fruit.SortMetric.Mass );
Console.WriteLine( "" );
Console.WriteLine( "The basket is holding:" );
foreach( Fruit fruit in fruitBasket )
{
Console.WriteLine( " a(n) " + fruit.Name +
" w/ Mass = " + fruit.Mass );
}
Console.WriteLine( "" );
Console.WriteLine( "Sorting by Color" );
fruitBasket.Sort( Fruit.SortMetric.Color );
Console.WriteLine( "" );
Console.WriteLine( "The basket is holding:" );
foreach( Fruit fruit in fruitBasket )
{
Console.WriteLine( " a(n) " + fruit.Name + " w/ Color = "
+ fruit.Color );
}
它奏效了,但在 Sort
方法中声明这些内容仍然有点粗糙。
Example9.cs
因为我们在另一个类的深处调用 Sort
例程,所以代码有点难看也没关系。在我们实际进行调用的级别,实际的 Array.Sort
被嵌入到 switch
/case
结构中,不需要多次输入。不幸的是,情况并非总是如此。我们可以使用 static
访问器来实例化类并进行整理。在 FruitBasket
中添加这两个属性。
internal static IComparer SortByMass
{
get
{
return( (IComparer) new Fruit.SortByMassClass());
}
}
internal static IComparer SortByColor
{
get
{
return( (IComparer) new Fruit.SortByColorClass());
}
}
现在在实际调用 Array.Sort
的 Sort
方法中,您可以简单地引用该属性。
case Fruit.SortMetric.Mass:
Array.Sort( basket, 0, count, SortByMass );
break;
case Fruit.SortMetric.Color:
Array.Sort( basket, 0, count, SortByColor );
break;
作为一个额外的优势,因为接口在属性中被重铸,我们大大简化了 Sort
方法中的调用,使代码更具可读性和可管理性。
Example10.cs
虽然这已经对 IEnumerable
、IEnumerator
、IComparable
和 IComparer
进行了全面、高度涵盖的概述,但您还有更多可以(也应该)做的事情。
public class FruitBasket : IEnumerable
{
Fruit[] basket = new Fruit[1];
int count = 0;
int revision = 0;
internal Fruit this[int index]
{
get
{
CheckIndex( index );
return( basket[index] );
}
set
{
CheckIndex( index );
basket[index] = value;
revision++;
}
}
...
internal void CheckIndex( int index )
{
if( index >= count )
throw new ArgumentOutOfRangeException(
@"Index value out of range" );
}
...
public class FruitBasketEnumerator : IEnumerator
{
FruitBasket fruitBasket;
int index;
int revision;
#region IEnumerator Members
public void Reset()
{
index = -1;
revision = fruitBasket.Revision;
}
public object Current
{
get
{
if( revision != fruitBasket.Revision )
throw new InvalidOperationException(
@"Collection modified while enumerating." );
return( fruitBasket[index] );
}
}
为了使它更具线程安全性,FruitBasket
和 FruitBasketEnumerator
都维护了一个 revision
计数器。如果 revision
值不同,那么您就不能再确定您拥有有效数据了。
还设置了保护措施,以确保在向数组写入或从数组读取时,index
处于合法范围内。因为该范围受到 count
的限制,所以您不能使用该属性来添加 Fruit
,只能通过 Add
方法来添加,但应该可以通过这种方式将一个 Apple
改成一个 Banana
。
结论
通过一些努力,在代码中实现接口可以大大扩展其功能。在此特定情况下,我知道我想实现 foreach
和 Array.Sort
,但实现一个接口就可能意味着我以前未曾考虑过需要的东西可能已经能够与我的类进行交互。
历史
- 版本 - 2005 年 3 月 6 日
原始文章。