这篇文章是“七种武器”系列的一篇,在这篇导言中有相关的介绍和代码,读者应该先读一读它。

目录

C#和Java相似:它们都把程序编译成某种“字节码”,然后在某种“虚拟机”上执行该字节码。此外,它们的语法形式都深受C++影响1。另外,它们还是相互竞争的关系。因此,把它们放在一起比较是有意义的。

C# Namespace VS Java Package

它们都是对应语言用于组织名字空间(namespace)的工具,但形式上很不相同:

  • Java要求目录结构与Package对应,比如对于Package io.huiming.hangman的目录结构必须是io/huiming/hangman,所有在这个Package下的类型的实现文件都必须位于那个目录下。C#的Namespace则不然:名字空间Io.Huiming.Hangman下的类型的实现文件可以在任何地方。
  • Java通过import关键字(keyword)导入一个类型,如import java.util.List;,或者一个包下的所有类型,如import java.util.*;;但C#的using指令(directive)一般用于导入整个Namespace下的类型,如using System,只有在发生名字冲突的情况下才需要单独导入某个类型,如using Console2 = System.Console——这条指令同时给System.Console起了一个别名Console2

类定义(Class)

C#与Java的类定义语法大同小异,但是C#提供了更多的语言设施,比如属性(Property)和索引(Indexer)。

Java需要通过getter和setter方法这种“约定俗成”的方式来定义属性:

public String getGuessedSoFar() {
  return new String(guessedSoFar);
}

在C#中可以更简单:

public string GuessedSoFar => new string(guessedSoFar);

对于有多行代码的属性:

public ISet<char> AllGuessedLetters {
  get {
    ISet<char> guessed = new HashSet<char>();
    guessed.AddAll(correctlyGuessedLetters);
    guessed.AddAll(incorrectlyGuessedLetters);
    return guessed;
  }
}

同时支持读写的属性:

private int count;

public int Count {
  get { return count; }
  set { count = value; }
}

索引可以使我们像访问数组元素那样访问一个对象的元素。例如,对如下Java代码:

//String secretWord = ...
//char[] guessedSoFar = ...
for (int i = 0; i < secretWord.length(); i++) {
  guessedSoFar[i] = secretWord.charAt(i);
}

相应的C#代码是:

//string secretWord = ...
//char[] guessedSoFar = ...
for (int i = 0; i < secretWord.Length; i++) {
  guessedSoFar[i] = secretWord[i];
}

由于Java没有类似C#的Indexer,所以只能通过charAt方法来访问字符串中的字符,而C#的string由于有Indexer,所以可以像访问数组元素那样访问字符串中的字符secretWord[i]

定义一个Indexer类似这样:

//char[] content = ...
public char this[int i] {
  get { return content[i]; }
  set { content[i] = value; }
}

最后,Java要求一个文件最多只能定义一个公共类(public class),并且文件名要与公共类的名字相同,C#则没有这样的限制。

嵌套类(Nested Class)

C#的嵌套类与Java不大相同。如下Java代码:

class MyGuessingStrategy implements GuessingStrategy {
  private static class WordSet extends AbstractCollection<String> {
    //...
  }
}

相当于如下C#代码:

public class MyGuessingStrategy : IGuessingStrategy {
  private class WordSet : ICollection<string> {
    //...
  }
}

注意上面的C#代码:嵌套类WordSet之前并无static修饰符,因为C#嵌套类都相当于Java的static修饰的嵌套类。

此外,C#并没有Java那样的非static嵌套类,你需要在嵌套类里保存一个外部类引用,如果用得到的话。因此,如下Java代码:

private static class WordSet extends AbstractCollection<String> {
  private class WordIterator implements Iterator<String> {
    private Iterator<String> it = WordSet.this.words.iterator();
    //...
  }
}

对应的C#代码是:

private class WordSet : ICollection<string> {
  private class WordIterator : IEnumerator<string> {
    private readonly IEnumerator<string> it;

    public WordIterator(WordSet outer) {
      it = outer.words.GetEnumerator();
    }
    //...
  }
}

另外需要注意的是,C#的类定义,不论是否嵌套,都可以用static来修饰,但它的含义是:所修饰的类只含有static成员。

类型(Type)

Java的类型可分为primitive类型(包括intdoublecharboolean等共8种)与非primitive类型(所有从java.lang.Object派生而来的类,包括java.lang.Object本身)。primitive类型都是所谓的“值类型(Value Type)”,非primitive类型则是“引用类型(Reference Type)”。C#的类型系统与Java有相似之处但又很不一样,我们可以通过代码来比较:

以下Java代码

private Set<Character> correctlyGuessedLetters = new HashSet<Character>();

对应的C#代码是

private ISet<char> correctlyGuessedLetters = new HashSet<char>();

要在Java代码中定义一个char Set,我们必须用Set<Character>而不是Set<char>。这是因为Java的范型(Generic)只支持引用类型,而char是值类型,所以必须用char对应的引用类型Character。Java的primitive类型都是值类型、都有对应的引用类型,又称为“包装类(Wrapper)”2

C#与此不同:它没有所谓的包装类,因为它的范型既支持引用类型也支持值类型。

此外,C#还允许用户通过struct关键字定义自己的“值类型”——实际上,它的primitive类型,如intchar等,都是系统定义的struct。而且不论“值类型”还是“引用类型”,都派生自System.Object。因此C#的类型系统比Java更加统一。

C#显式接口实现(Explicit Interface Implementation)

这是C#专有的一个特性,用以解决来自两个不同接口的方法签名冲突的问题(当你需要在C#中实现一个集合(ICollection)时就要用到,见下文)。举例来说:

interface IA {
  void Foo();
}

interface IB {
  int Foo();
}

class C : IA, IB {
  //? Foo()
}

C同时实现了IA和IB,但这样实现是不合法的:

class C : IA, IB {
  public void Foo() {}
  public int Foo() { return 1; }
}

你不能重载一个方法——仅仅是返回值不同。这时就需要显示接口实现:

class C : IA, IB {
  public void Foo() {}

  int IB.Foo() { return 1; }
}

注意IB.Foo()之前不可以有public修饰符。

你可以在C或者IA上调用void Foo(),或者在IB上调用int Foo(),像这样:

  C c = new C();
  c.Foo(); //Call void Foo()
  IA a = c;
  a.Foo(); //Call void Foo()
  IB b = c;
  b.Foo(); //Call int Foo()

C#扩展方法(Extension)

这也是C#的一个专有特性。当你在MSDN上查看某个类型的文档时,可能会发现其下有大量并不属于该类型定义本身的“Extension Methods”,比如对ICollectionAggregateFirst……都是扩展方法。

我们的C#算法实现也在ICollection接口上定义了一个扩展方法AddAll

public static class MyExtension {
  public static void AddAll<T>(this ICollection<T> to, ICollection<T> from) {
    foreach (T e in from) {
      to.Add(e);
    }
  }
};

AddAll方法的第一个参数带有修饰符this,表示这个方法可以扩展到ICollection<T>类型的对象上。然后就可以这么使用它:

public ISet<char> AllGuessedLetters {
  get {
    ISet<char> guessed = new HashSet<char>();
    guessed.AddAll(correctlyGuessedLetters);
    guessed.AddAll(incorrectlyGuessedLetters);
    return guessed;
  }
}

就好像这个方法是定义在ICollection上一样。

需要注意的是:扩展方法不可以在被扩展对象的构造函数里使用,所以:

class WordSet {
  public WordSet(string pattern, ISet<char> guessedLetters, ICollection<string> words) {
    //...
    //这里不可以使用 this.AddAll(words)
    MyExtension.AddAll(this, words);
    //...
  }
}

集合与迭代(Collection and Iteration)

这里的集合是指实现了特定接口的对象。对Java而言它是java.util.Collection

public interface Collection<E> extends Iterable<E>

对C#是System.Collections.Generic.ICollection

public interface ICollection<T> : IEnumerable<T>, IEnumerable

集合是一个很重要的概念,我们日常所用的List、Set和Map3都是集合。集合有一些共同的操作/方法,围绕集合还有一些通用的算法,如排序。

迭代及其接口

集合最基本的操作是迭代,或者说枚举其中的元素。对此Java和C#都有相应的语言支持:

Java使用增强的for语句来迭代:

//Set<String> dict = ...
List<String> words = new ArrayList<String>();
for (String word : dict) {
  if (word.length() == len) {
    words.add(word);
  }
}

C#使用foreach

//ISet<string> dict = ...
IList<string> words = new List<string>();
foreach (string word in dict) {
  if (word.Length == len) {
    words.Add(word);
  }
}

Java和C#集合对象可以迭代的关键是它们都实现了某种可迭代接口:

对于Java它是java.lang.Iterable

public interface Iterable<T> {
  Iterator<T> iterator();
  //...
}

对于C#则是System.Collections.Generic.IEnumerable

public interface IEnumerable<out T> : IEnumerable {
  IEnumerator<T> GetEnumerator();
}

可迭代接口的关键是返回一个迭代器:

Java Iterator

public interface Iterator<E> {
  boolean hasNext();
  E next();
  //...
}

C# IEnumerator

public interface IEnumerator<out T> : IDisposable, IEnumerator {
  bool MoveNext();
  T Current { get; }
  //...
}

实现一个集合

当我们要实现一个自己的集合时,就必须实现可迭代接口,以及其他一些必要的集合方法。

在这方面,Java要更容易一些:Java提供了一个抽象类java.util.AbstractCollection,只要实现了iteratorsize方法就能实现一个集合,例如:

private static class WordSet extends AbstractCollection<String> {
  private class WordIterator implements Iterator<String> {
    //...
  }

  @Override
  public boolean add(String word) {
    //...
  }

  @Override
  public int size() {
    return words.size();
  }

  @Override
  public Iterator<String> iterator() {
    return new WordIterator();
  }
}

WordSet还实现了add方法用以向集合中添加元素。

在C#里实现一个集合就比较麻烦:C#没有提供类似的抽象类,所以你要从头开始实现ICollection的每个方法(好在它们也不算多),另外,由于ICollection<T>同时继承了IEnumerable<T>IEnumerable,而这两个接口中存在签名相同的方法,所以你必须要用到上文提到的显示接口实现(Explicit Interface Implementation)。对于IEnumerator<T>来说也有同样的问题。

最终,与上面Java代码对应的C#代码像这样:

private class WordSet : ICollection<string> {
  private class WordIterator : IEnumerator<string> {
    public string Current => it.Current;

    //Explicit Interface Implementation
    object IEnumerator.Current => it.Current;

    //...
  }

  public int Count => words.Count;

  public void Add(string word) {
    //...
  }

  public IEnumerator<string> GetEnumerator() {
    return new WordIterator(this);
  }

  //Explicit Interface Implementation
  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator();
  }

  //Other methods in ICollection...
}

集合上的操作/方法

Java和C#都对集合提供了很多通用算法,如排序、查找等。Java的java.util.Collections类用静态方法提供了这些支持。C#通过上文提到的扩展方法提供支持,比如对于ICollection<T>,这些扩展多来自于System.Linq.Enumerable

总结

我们的对比只限于一些基本方面,也并非要一分伯仲,实际上总的来说,这两种编程语言在伯仲之间:

  • Java提供的语言设施要精简一些、C#更加丰富,比如属性、索引、扩展方法等。这对用户来说是好事,因为使用起来方便。但坏的方面是这增加了语言的复杂度以及你掌握它的成本。
  • C#提供了某些Java无法提供的解决方案,比如显式接口实现。这很好,但同上一条一样,这种方案不是没有成本的,它使语言更加复杂,然而它解决的问题可能并不是日常会遇到的,不过由于它在.Net集合框架的实现中被大量使用,所以就人为地成了日常会遇到的问题。
  • C#的类型系统更加统一。这对于使用范型来说明显比Java更加统一、方便。
  1. 这不是没有原因的:Java作者曾经想给它命名为C++++--,意思是给C++加上一些好东西、去掉一些不好的东西。C#用一个“#”表示4个“+”号,意思是C++的加强版。 

  2. 读者可能会想:既然Character的使用范围比char更广泛,那不如在所有用到char的地方都使用Character就好了。但这样并不好,因为Java虚拟机操作primitive类型比它的wrapper更高效。另外,在primitive类型和它的wrapper之间进行转换还涉及到所谓boxing/unboxing的问题。 

  3. Java的Map比较特殊:它虽然也是集合但并不从Collection接口派生。它的entrySet方法返回一个“键-值”Set可以进行for迭代。