Java作为一种编程语言如此普及的原因之一,是它解决了其他语言中常见的I/O、网络操作中非常困难的问题,C#语言也采用了与Java类似的方法,提供了一些库来完成对I/O和网络的操作,并隐藏了其复杂的实现方法。本篇文章将着重讨论C#中与I/O、网络操作有关的名字空间以及这些库的一些模板的通用用法。为了方便熟悉Java的编程人员更好地掌握C#,使广大读者能够对二种语言进行比较,在本篇文章中我们也都给出了相同功能在Java中的实现方法。
理解流操作
Java和C#中的流通常与从控制台、文件系统或网络读取数据或向这些设备写数据有关,在这二种语言间,如果程序需要移动或对许多字节的数据进行操作,都会较多地用到流操作。
Java提供了二个抽象类:java.io.InputStream和java.io.OutputStream,其中包括一些允许程序读或写流数据的方法。C#则把这二个类合并为一个:System.IO.Stream,与有一个用于读流数据、一个用于写流数据的二个对象不同,C#中的Stream对象需要通过测试CanRead 、CanWrite属性对其读、写能力进行检测。
同步I/O操作
同步I/O操作在这二种语言中是十分相似的,无论是Java中的java.io.InputStream和 java.io.OutputStream还是C#中的System.IO.Stream都有每次只对一个字节数据进行操作的方法,也有每次对一批数据进行操作的方法。(C#缺乏一次对一个字节数组操作的能力,只能通过偏移量/长度参数对字节数组进行操作。)
表1:Java和C#中的同步I/O操作
Java编程人员需要注意的一点是:不要忘记IOException。与Java不同,C#编译器在编译时不要求处理异常情况。
Java缺乏完成异步I/O操作的正式方法,它没有"内置"的方法对流进行读、写,然后再检查其结果。其中最"相似"的一种模拟是在同步操作时产生一个java.lang.Thread线程,使得该线程引起侧放作用(某些指令取出后,暂时不执行,放在一边)或完成检查工作。C#的库中有内置的异步I/O操作命令。
例如,要在Java中执行一个可以对状态进行检查的异步read( byte[] b )操作,下面是一种可能的实现方法:
file:// 保存读操作的侧放作用的变量
int read; file:// 保存read的结果
IOException exception; // 保存可能发生的异常
Object wait ...
// 在检查结束前需要保留的一些值
file://InputStream变量is中有关read的封装
( new Thread( new Runnable() {
public void run() {
try {
read is.read();
} catch( IOException error ) {
exception error;
} finally {
synchronized( wait ) {
file:// 唤醒等待这一变量的其他进程
// 执行read操作
wait.notifyAll();
}
// 调用检查方法
callback();
}
}
} ) ).start();
这将使得read的值或执行读操作的异常情况被分别存储在read和exception中,另一个依赖于变量wait的进程可能继续等待或完成检查的方法。
C#提供了封装了上述全部功能的BeginRead和EndRead这二个方法,BeginRead的作用与Read类似,但它对二个或更多的变量━━一个AsyncCallback变量和一个状态对象进行操作,返回一个可以稍后用来检查异步读进程的IAsyncResult对象。标准的BeginRead的用法与下面的用法相似:
IAsyncResult iar sbs.BeginRead( buffer, 0, 1,
new AsyncCallback( = callback ), null );
带callback方法的用法如下:
public void callback( IAsyncResult iar )
要检查实际读取了多少字节,可以调用带IAsyncResult对象参数的EndRead方法。需要记住的是,在BeginRead执行完毕之前,EndRead的调用将被阻塞。要在没有阻塞的情况下检查read的状态,可以检查返回的IAsyncResult变量的IsCompleted属性。另外需要注意的是,在异步read完成之前,缓冲区中的内容是不可靠的。
实现流操作
Java和C#中的流数据非常地相似。如果对Java中的流操作足够熟悉,在C#中完成流操作就不会是太困难的事儿了。Java和C#中实现流操作的最主要的差别不仅仅是需要实现的适当的读或写方法,还有C#中的Stream类还可以作为"读者"或"作者"和准确反映Stream容量的属性。
表2:Java和C#中Stream操作的实现
C#中的Stream类在完成某一种功能时提供了多种选择,最重要的Read和Write(二者都需要一个字节数组、偏移量、长度三个参数)这二个方法就足够了,因为所有方法的执行都会用到其他的方法,简单的read/write方法将能够给流添加所需要的功能。缺省的ReadByte和WriteByte的执行方式将把long型变量与字节数组之间进行转换,异步执行的BeginRead和BeginWrite方法将在独立的线程中执行Read或Write方法。
这篇稿子的大部分篇幅谈论的都是C#中的System.IO.Stream类有关,在这里我们还需要讨论一下System.IO.TextReader和System.IO.TextWriter这二个类,这二个类与Java处理I/O的方式十分相似,其中的一个类负责处理读取操作,另一个类则负责处理写操作。C#中的Stream对象掩盖了如何读取、写的有关细节,TextReader和TextWriter则分别独立地存储着读取或写入的字节。由上述二种类衍生出的最通用的类是System.IO.StreamReader和System.IO.StreamWrtiter类,这二个类都可以对一个Stream对象进行操作,System.Text.Encoding对象指定一个字节流如何转换为字符流。(缺省情况下,C#使用UTF-8进行编码/解码。)
如果需要使用与流类似的功能,而且需要对字符而不是字节进行操作,使用TextReader和TextWriter二个类的子类要比使用Stream类简单得多。虽然如果Stream动用得当,也可以使用StreamReader和StreamWriter类实现流操作。
文件系统I/O
在Java中完成磁盘操作是十分简单的,通常情况下它就是操作java.io.File对象和使用java.io.FileInputStream或java.io.FileOutputStream。跟我们在上面看到的一样,C#在许多方面与Java相同,但也有一些不同之处。
与Java一样,C#的文件对象与底层的文件系统之间并没有特别紧密的联系。我们可以为一个不存在的文件创建一个File对象,也可以为存在的文件创建一个File对象,并在C#不知道的情况下将它移到别处。由于拥有CreateText或AppendText等向文件系统返回一个流的静态方法,因此,与Java不同的是,C#中的File对象的作用要重要得多。
在Java中要创建一个新文件或向文件写内容,则必须使用FileInputStream:
FileOutputStream fos = new FileOutputStream( "brand-new-file.txt");
fos.write( ... )
但在C#中就可以使用:
Stream s = File.Create( "brand-new-file.txt" );
或者使用:
StreamWriter sw = File.CreateText( "brand-new-file.txt" );
来得到一个新文件的Stream或StreamWriter。(Java中的添加可以通过设置FileOutputStream的构建者之一的append属性来完成。)Java允许使用java.io.FileInputStream从文件中读取内容,而C#则有Open Write和OpenText等静态的方法。最后,C#在Open方法中提供了更详细的控制━━这一方法允许设置文件的权限和访问环境。
表3:操作文件读写的方法
C#中值得一提的改进是File.Copy方法。困扰大多数Java编程人员的一个与文件系统I/O有关的问题是不能正确地移动文件,java.io.File中包含一个可以对文件重新命名的renameTo方法,但它只在文件系统内有效。大多数情况下,编程人员都必须编写自己的文件移动命令,一般是利用java.io.FileInputStream和java.io.FileOutputStream拷贝文件,然后再删除原来的文件。C#中的Copy方法可以很方便地移动文件,但File.Move也不能跨卷和文件系统移动文件。
C#的文件系统无须处理Java必须处理的跨平台问题,因此也就没有与java.io.File.pathSeparator或java.io.File.separator功能类似的对象。不幸的是,这也就意味着在C#中不存在受到广大编程人员喜爱的java.io.File构造器:
public File( File parent, String child )
使用C#的编程人员可以使用下面的命令来构造一个新的System.IO.File对象:
File parent ...
File child new File( parent.FullName + "\\" + childName );
Understanding Networking
二种语言都围绕着基本协议提供了一些抽象层,Java中的java.net.Socket类的抽象程度要远高于C#中的System.Net.Sockets.Socket类。
Java和C#都提供了对网络的不同抽象层,编程人员可以使用不同的网络接口完成对网络的操作。
表4:Java和C#中的网络层次
应答/请求层可以用于HTTP类的请求,其中的一端开始启动一个连接,发送一些字节的数据,然后停止,等待对方作为应答发回的一些字节。对于象流这样更灵活的操作,协议层的用处更大。对于大多数的Java编程人员来说,除非需要完成性能非常高的网络操作,不需要对套接字进行直接控制。如果需要。C#仍然提供了对原始的Berkeley套接字进行控制的能力。
应答/请求层:
这个层次抽象掉了所有网络层的细节,提供了一个可以双向传输数据的象流那样的接口。Java可以接受HTTP URL,并通过下面的命令完成GET命令:
URL url= new URL( "http://to.post.to.com" );
URLConnection urlConnection url.openConnection();
InputStream input urlConnection.getI