
2006年4月3日
一种Internet策略
ActiveX是以一种Internet策略出现的。它包含了OLE,COM及Internet开发的各个方面。
ActiveX开发涉及范围广泛 ActiveX开发的包含了许多方面,比如自动化服务器,ActiveX控件,COM对象等等。
术语ActiveX在过去的几年中成为了许多开发人员及团队的战斗口号,另一方面市场也对其大肆宣扬,然而,没有几个人能解释清楚其确切的含义。本书主要目的就是说明什么是ActiveX以及它对开发人员意味着什么。我希望读者能够学到和我为写本书而学到的知识尽可能一样多。
应用程序开发的一种Internet策略
Microsoft第一次介绍ActiveX是在1996年的Intenet专业开发人员大会(Internet PDC)上。ActiveX源自于大会的口号“Activate the Internet”(可理解为:让因特网活跃起来),与其说ActiveX是一种开发应用程序的技术或是架构,不如说它是一种启示。
在开发大会期间,Microsoft正在与控制了Internet浏览器市场的Netscape进行激烈的竞争。但是,大会表明了Microsoft感兴趣的不仅仅是浏览器市场。Microsoft演示的工具从电子存储前端产品、新的OLE控件到虚拟现实聊天软件等等,应有尽有。
ActiveX是Micrsoft共同的新口号,类似于90年代初的提出的OLE(Object Linking and Embedding,对象链接与嵌入),而且在很短的时间内,远远超越了“Activate the Internet”。
ActiveX成为了包含一切的术语:从Web页面到OLE控件。ActiveX开始变得重要起来:一方面,小型、快速、可重用的组件能够让你紧紧抓住来自于Micrsoft,Internete及工业的最新技术;另一方面,ActiveX代表了Internet与应用程序集成的策略。目前,在产品或公司中没有使用Internet及ActiveX技术被认为是过时的。事实上,描述ActiveX就像描述色彩一样,它既不是技术也不是架构,而是一个概念,一个指导。
ActiveX, OLE及Internet
ActiveX和OLE开始成为同义词,人们曾经谈到的OLE控件现在成为了ActiveX控件,OLE文档对象现在成为了ActiveX文档对象。有时,整个关于如何实现OLE技术的文档被更新为ActiveX技术,唯一的变化就是术语OLE,现在命名为ActiveX。
尽管OLE及ActiveX取得了巨大的进步,表面上每天还有与其相关的新技术出现,但Internet是否已经或直接卷入到许多相关的领域还是令人置疑的。对小型、快速、可重用组件(COM组件)的需求已经些年头了,分布式组件(DCOM组件)在几年前的OLE 2.0 专业人员开发大会上作了第一次演示。Visual Basic(VB)开发组在使ActiveX技术成为可能的早期扮演了得要角色。包含在ActiveX SDK中的BaseCtl框架就是由VB开发组开发的,它解决了VB为减少载入时间而对小型,轻量级组件的需求。Internet唯一的贡献就是它需要一种方式来实现和发布Web页面。实际上,每一个ActiveX的新功能都能追溯到最基本的,全球泛围的对小型、快速,可重用组件的需求,而这,就是从OLE和COM开始的。
ActiveX并不意味着要代替OLE,仅仅把它扩大到包括Internet,企业内部网商务应用程序及家庭应用程序的开发,以及开发这些应用的工具。
Microsoft发布了大量关于ActiveX开发的文档。OC 96 规范定义了如何开发启动更快速,绘制能力更强的控件,它也定义了哪些接口是必需的,而哪些接口是可选的。”OLE Control and Control Container Guidelines”提供了关于控件与控件容器交互的重要信息。Microsoft的Web站点成为了信息丰富的及创建、使用、分发ActiveX组件的中心。
除了创建ActiveX组件的技术细节外,Microsoft建立起了一套使用和集成ActiveX组件的标准。从VB、Micrsoft Word到Java的每一个产品都继承了使用ActiveX组件的能力。在ActiveX技术出现前,一大半的应用程序无法像如今这样如此紧密相关地无缝集成。
接下来的部分将谈到我们可以创建的ActiveX组件的类型,以及我们何时,为什么才需要使用它—这可能更有帮助。
ActiveX组件的分类
本书的主题是ActiveX组件的开发。这些组件可以分为以下几类:
Automation Servers:自动化服务器
Automation Controllers:自动化控制器
Controls:控件
COM Objects:COM对象
Documents:文档
Containers:容器
本书只是详细谈到了自动化服务器,控件及COM对象。自动化控制器、文档及容器涉及到太多的接口,太多的技术,超出本书所能承受的范围。
自动化服务器
自动化服务器是可以被其它应用程序编程驱动的组件。一个自动化服务器包含到少一个或多个基于IDispatch的接口,其目的是为了让其它的应用程序创建和连接它。根据服务器本身的特性,一个自动化服务器可以包含也可以不包含用户界面。
自动化服务器可以是进程内的(运行在控制器的进程空间),本地的(运行在自己独立的进程空间),远程的(运行在其它机器的进程空间)。有些情况下,特定的服务器实现将会指定服务器在哪里运行,但是,这一点是不能保证的。DLL能够在进程内,本地或远程运行,而EXE则只能在本地或远程远行。
注意:对于控制器来说,执行最快的就是进程内自动化服务器。但要记住,使用进程内服务器并不能保证其性能。如果一个进程内自动化服务器在一个进程空间内被创建,而被另一进程内的控制器所控制,它就降级为进程外服务器,其性能与进程外服务器相同。关于进程空间与服务器冲突的更多信息请参见本书的第二部分。
自动化控制器
自动化控制器是那些能使用和操作自动化服务器的应用程序,一个很好的例子就是VB。使用VB你可以创建,使用并销毁自动化服务器,就好像它们是VB语言的完整一部分一样。
自动化控制器可以是任意类型的应用程序,DLl或是EXE,能够以进程内,本地,远程的方式访问自动化服务器。一般地,注册表入口与服务器的实现指明了与控制器相关的自动化服务器应该在哪一个进程空间运行。
控件
ActiveX等同与OLE控件或是OCX。一个典型的ActiveX控件由一个在设计时及运行时都存在的用户界面,一个定义了控件的所有方法及属性的IDispatch接口,一个定义了控件可以触发的事件的IConnectionPoint接口所组成。此外,它还可能支持运行生命期内的持久化,以及各种用户界面功能,例如,剪切粘贴,拖放操作。从结构上来讲,一个控件必须支持大量的COM接口以发挥这些功能的优势。
随着新的OLE控件及ActiveX开发指南的发行,一个控件不仅限于上述那些功能。但开发人员可以仅仅实现上述的那些功能,因为它们用处最大,而且对于使用应用程序的用户来说,他们也最感兴趣。由Microsoft出版的控件与容器指南列出了所有的接口以及它们的特殊要求。你可以在Microsoft的网站http://www.microsoft.com找到这些信息。
ActiveX控件对于容器来说,总是进程内运行的。一个控件的扩展名通常是OCX,但从执行模式上来讲,它就是一个DLL而已。
COM对象
Com对象在结构上类似于自动化服务器和控制器。它们包含一个或多个COM接口,有一部分或完全没有用户界面。然而,这些对象不能被控制器以使用自动化对象的方式所使用。控制器必须理解COM接口才能与这些接口通讯,这些都与自动化接口无关。Windows 95和NT操作系统定义了成百上千的COM接口和自定义接口作为操作系统扩展,控制了从桌面外观到屏幕三维图像渲染的一切。COM对象是一种组织相关功能及数据的很好的方式,并且仍然保留了DLL对高性能的要求。
注意:自动化服务器也能受益于COM接口,这些服务器就是双接口服务器。自动化服务器接口有一个伴随的COM接口,它描述了对象的方法及属性。象VB这样的自动他控制器在使用服务器的时候能够利用双接口的优势提供更高的性能。双接口服务器有一个缺点就是在定义属性和方法时,其数据类型被限制为被OLE自动化所支持的类型。
文档
ActiveX文档或者说是最初被称为的文档对象,表示那些不仅仅只是一个控件或服务器的对象。一个文档可以是任意的,可以是电子表格或者是一个财务应用程序中的复杂的发票。文档,就像控件,有用户界面,并且有容器应用程序作为宿主。Microsoft Word和Microsoft Excel就是复杂的文档服务器,Microsoft Office Binder及Microsoft Internet Explorer就是ActiveX文档容器。
ActiveX文档构架是对OLE链接与嵌入模型的扩展,它允许文档透过其宿主容器得到更多的控制,最明显的改变就是菜单如何呈现。标准OLE文档的菜单将会与容器的菜单合并,提供组合的功能集;而ActiveX文档则会占用整个菜单系统,因此只表示文档的功能而不是同时表示文档和容器的功能。事实上,文档所暴露的功能集是ActiveX文档与OLE文档之间差别的前提。容器只是一种宿主机制,而文档则拥有所有的控制。
其它的不同就是打印和存储。OLE文档有意地成为其宿主容器文档的一部分,因此,作为容器文档的一部分来打印与存储。而ActiveX文档则期望支持其本身的打印与存储功能,并没有与宿主文档集成。
ActiveX文档采用统一的表示结构,而不是OLE文档的嵌入式的文档架构。Microsoft Internet Explorer就是一个很好的例子,它只不过给用户显示Web页面,但是将页面作为一个实体进行浏览、打印及存储。Microsoft Word和Microsoft Excel则是OLE文档架构,如果Excel的电子表格嵌入到Word文档,那么电子表格将与Word文档一起存储,成为完整Word文档的一部分。
ActiveX文档还有一些额外的功能用来支持Internet或Internat的Web页面发布。想像一下一个内部的订单跟踪系统运行在与连接Internet所使用的相同的Web浏览器中。
容器
ActiveX容器是能够作为ActiveX服务器、控件或文档的宿主的应用程序。VB和ActiveX Control Pad都是能够作为ActiveX服务器和控件的容器。Microsoft Office Binder和Microsoft Internet Explorer则是能够作为ActiveX服务器、控件及文档宿主的容器。
随着ActiveX控件和文档规范中对必要要求的减少,容器必须足够健全以处理控件或文档缺少某些接口的情况。容器应用程序可能与控件或文档只有很少或根本没有交互,也可能在表现和操作组件时提供了很重要的交互能力。这种能力完全依赖于组件的宿主容器,在任何的容器开发中都不是必要的。
posted @ 2006-04-03 16:02 Jingu 阅读(1485) 评论(0)
编辑

2006年3月23日
一 介绍委托和事件
事件是特殊化的委托,委托是事件的基础,所以在介绍事件之前先介绍一下委托。
委托:使用委托可以将方法应用(不是方法)封装在委托对象内,然后将委托对象传递给调用方法的代码,这样编译的时候代码就没有必要知道调用哪个方法。通过使用委托程序能够在运行时动态的调用不供的方法。
事件:事件是对象发送的消息,发送信号通知客户发生了操作。这个操作可能是由鼠标单击引起的,也可能是由某些其他的程序逻辑触发的。事件的发送方不需要知道那个对象或者方法接收它引发的事件,发送方只需知道在它和接收方之间存在的中介(Deletgate)
二. 声明事件(定义事件)
在GUI图形界面中的事件(单击按钮或者选择菜单等)是已经定义好的事件,所需要的就是注册事件。我们也可以自己定义声明事件,可以将自己定义的事件应用到程序的任何地方。事件和委托是分不开的,c# 中使用委托来声明事件。
如果在类里面声明事件,那么必须先声明该事件的委托类型,也可以使用一个事先已经声明好的委托类型。
下面是一个声明事件的例子:
using system;
public delegate void MenuHandler()//事先申明一个委托,也可以在类的内部声明委托类型
public class Menuitem //声明一个菜单项目的类
{
public event MenuHadler MenuSelection;// 声明一个事件并指定它的委托类型
string text ;//声明一个字符串
public MenuItem(string text) //菜单项目类的构造函数
{
this.text=text; // 初始化text字符串
}
public void Fird()
{
MenuSelection; //定义一个触发事件的方法(后面将用到)
}
public string Text //定义属性
{
get
{
return text;
}
set
{
text=value;
}
}
}
程序首先定义个一个叫做MenuHandler的委托,然后定义了叫做 MenuItem 的类,MenuItem类包含了一个MenuSelection 事件(菜单被选择事件),事件的委托类型是MenuHandler。事件的声明完成啦,下面开始注册事件
三.注册事件
这里先讲一下 publisher/subscriber(发布者/订阅者)模式,因为事件就是从这个模式继承下来的。一個訊息的傳送者 (Sender) 稱之為 Publisher,而該訊息的接收者 (Receiver),則稱之為Subscriber,Subscriber(订阅者)在Publisher(发布者)处注册自己,Publisher发生改变的时候,通知Subscriber做适当的操作,很类似设计模式中的Observer 。
在这里我们认为注册的程序就是订阅者(Subscriber)。事件相当于发布者(Publisher),下面的程序表明了如何把订阅器连接到发布器:
using system;
public class DelegateAndEvents //定义个注册事件的类
{
public static void int Main ()
{
SiteManager sm=new SiteManager (); // SiteManager是一个站点管理类
MenuItem AddMenu= new MenuItem(“Add”) //定义一个新的事件类
MenuItem DelMenu=new MenuItem(“Delete”) //定义另外一个新的事件类
//通过委托,sm类的Addsite 方法 注册到事件中
AddMenu.MenuSelelction+=new MenuHandler(sm.AddSite)
//通过委托,sm类的DeleteSite 方法 注册到事件中
DelMenu.MenuSelelction+=new MenuHandler(sm. DeleteSite)
}
}
从注册事件的那行代码来看,事件就像是事件类的一个字段(属性),但是对它的访问是很受限制的,只能通过:在该字段上撰写新的委托(+=)或者从字段(可能是复合字段)移除委托(-=)进行访问,
移除注册事件:DelMenu.MenuSelelction-=new MenuHandler(sm. DeleteSite)
四.实现事件
这里就要讲到上个例子中的SiteManager类,它包含了事件需要实现的方法(AddSite,DeleteSite),SiteManager类的方法一定要符合事件的委托类型和返回类型,在委托添加到事件前,该方法能够以这个方式付给委托。
using system;
public class SiteManger //站点管理类
{
public SiteManger() //构造函数 初始化变量
{
//impelement something
}
public void AddSite() //方法要符合委托的类型
{
// impelement 执行添加站点的操作代码
}
public void DeleteSite() //方法要符合委托的类型
{
// impelement 执行删除站点的操作代码
}
}
SiteManger类的方法要符合委托类型,这样他的方法可以很方便的当作事件的方法使用。
五. 触发事件
当调用事件时就可以说时触发了事件。事件是从定义它的类中触发的
示例
using system;
public class menu //定义一个菜单类
{
public menu(strng title) //类的构造函数
{
//初始化变量
}
public void Run() //触发事件的过程
{
//声明一个菜单项目对象(menuItem前面已经定义)
menuItem myMenuItem=new menuItem }
myMenuItem.fire //调用fire 方法触发事件,事件执行SiteManger类的方法
}
到此为止我就完成了整个事件的实现过程。
六.为事件增加“添加/删除”方法
如果由大量的方法需要注册到事件时,可以为为事件增加 Add,Remove 方法。通过访问事件的“添加/删除方法”来注册方法
示例:
using sytem;
pulic Delete ovid MenuHandler(object sender,EventArgs e); //示例1的变形
public class MenuItem
{
int numberof Events;
string tex;
private MenuHandler mh=null; //内部得一个委托
public event MenuHandler MenuSelecton
{
add //增加“添加”方法
{
mh+=value;
nuberofevents++;
}
remove
{
mh-=value;
numberofevents--;
}
}
posted @ 2006-03-23 12:45 Jingu 阅读(1018) 评论(1)
编辑
C#(C-Sharp)是Microsoft的新编程语言,被誉为“C/C++家族中第一种面向组件的语言”。然而,不管它自己宣称的是什么,许多人认为C#更像是Java的一种克隆,或者是Microsoft用来替代Java的产品。事实是否是这样的呢?
本文的比较结果表明,C#不止是Java的同胞那么简单。如果你是一个Java开发者,想要学习C#或者了解更多有关C#的知识,那么本文就是你必须把最初10分钟投入于其中的所在。
一、C#、C++和Java
C#的语言规范由Microsoft的Anders Hejlsberg与Scott Wiltamuth编写。在当前Microsoft天花乱坠的宣传中,对C#和C++、Java作一番比较总是很有趣的。考虑到当前IT媒体的舆论倾向,如果你早就知道C#更接近Java而不是C++,事情也不值得大惊小怪。对于刚刚加入这场讨论的读者,下面的表1让你自己作出判断。显然,结论应该是:Java和C#虽然不是孪生子,但C#最主要的特色却更接近Java而不是C++。
表1:比较C#、C++和Java最重要的功能
功能 C# C++ Java
继承 允许继承单个类,允许实现多个接口 允许从多个类继承 允许继承单个类,允许实现多个接口
接口实现 通过“interface”关键词 通过抽象类 通过“interface”关键词
内存管理 由运行时环境管理,使用垃圾收集器 需要手工管理 由运行时环境管理,使用垃圾收集器
指针 支持,但只在很少使用的非安全模式下才支持。通常以引用取代指针 支持,一种很常用的功能。 完全不支持。代之以引用。
源代码编译后的形式 .NET中间语言(IL) 可执行代码 字节码
单一的公共基类 是 否 是
异常处理 异常处理 返回错误 异常处理。
了解表1总结的重要语言功能之后,请继续往下阅读,了解C#和Java的一些重要区别。
二、语言规范的比较
2.1、简单数据类型
简单数据类型(Primitive)在C#中称为值类型,C#预定义的简单数据类型比Java多。例如,C#有unit,即无符号整数。表2列出了所有C#的预定义数据类型:
表2:C#中的值类型
类型 说明
object 所有类型的最终极的基类
string 字符串类型;字符串是一个Unicode字符的序列
sbyte 8位带符号整数
short 16位带符号整数
int 32位带符号整数
long 64位带符号整数
byte 8位无符号整数
ushort 16位无符号整数
uint 32位无符号整数
ulong 64位无符号整数
float 单精度浮点数类型
double 双精度浮点数类型
bool 布尔类型;bool值或者是true,或者是false
char 字符类型;一个char值即是一个Unicode字符
decimal 有28位有效数字的高精度小数类型
2.2、常量
忘掉Java中的static final修饰符。在C#中,常量可以用const关键词声明。
public const int x = 55;
此外,C#的设计者还增加了readonly关键词。如果编译器编译时未能确定常量值,你可以使用readonly关键词。readonly域只能通过初始化器或类的构造函数设置。
2.3、公用类的入口点
在Java中,公用类的入口点是一个名为main的公用静态方法。main方法的参数是String对象数组,它没有返回值。在C#中,main方法变成了公用静态方法Main(大写的M),Main方法的参数也是一个String对象数组,而且也没有返回值,如下面的原型声明所示:
public static void Main(String[] args)
但是,C#的Main方法不局限于此。如果不向Main方法传递任何参数,你可以使用上述Main方法的一个重载版本,即不带参数列表的版本。也就是说,下面的Main方法也是一个合法的入口点:
public static void Main()
另外,如果你认为有必要的话,Main方法还可以返回一个int。例如,下面代码中的Main方法返回1:
using System;
public class Hello {
public static int Main() {
Console.WriteLine("Done");
return 1;
}
}
与此相对,在Java中重载main方法是不合法的。
2.4、switch语句
在Java中,switch语句只能处理整数。但C#中的switch语句不同,它还能够处理字符变量。请考虑下面用switch语句处理字符串变量的C#代码:
using System;
public class Hello {
public static void Main(String[] args) {
switch (args[0]) {
case "老板":
Console.WriteLine("早上好!我们随时准备为您效劳!");
break;
case "雇员":
Console.WriteLine("早上好!你可以开始工作了!");
break;
default:
Console.WriteLine("早上好!祝你好运!");
break;
}
}
}
与Java中的switch不同,C#的switch语句要求每一个case块或者在块的末尾提供一个break语句,或者用goto转到switch内的其他case标签。
2.5、foreach语句
foreach语句枚举集合中的各个元素,为集合中的每一个元素执行一次代码块。请参见下面的例子。
using System;
public class Hello {
public static void Main(String[] args) {
foreach (String arg in args)
Console.WriteLine(arg);
}
}
如果在运行这个执行文件的时候指定了参数,比如“Hello Peter Kevin Richard”,则程序的输出将是下面几行文字:
Peter
Kevin
Richard
2.6、C#没有>>>移位操作符
C#支持uint和ulong之类的无符号变量类型。因此,在C#中,右移操作符(即“>>”)对于无符号变量类型和带符号变量类型(比如int和long)的处理方式不同。右移uint和ulong丢弃低位并把空出的高位设置为零;但对于int和long类型的变量,“>>”操作符丢弃低位,同时,只有当变量值是正数时,“>>”才把空出的高位设置成零;如果“>>”操作的是一个负数,空出的高位被设置成为1。
Java中不存在无符号的变量类型。因此,我们用“>>>”操作符在右移时引入负号位;否则,使用“>>”操作符。
2.7、goto关键词
Java不用goto关键词。在C#中,goto允许你转到指定的标签。不过,C#以特别谨慎的态度对待goto,比如它不允许goto转入到语句块的内部。在Java中,你可以用带标签的语句加上break或continue取代C#中的goto。
2.8、声明数组
在Java中,数组的声明方法非常灵活,实际上有许多种声明方法都属于合法的方法。例如,下面的几行代码是等价的:
int[] x = { 0, 1, 2, 3 };
int x[] = { 0, 1, 2, 3 };
但在C#中,只有第一行代码合法,[]不能放到变量名字之后。
2.9、包
在C#中,包(Package)被称为名称空间。把名称空间引入C#程序的关键词是“using”。例如,“using System;”这个语句引入了System名称空间。
然而,与Java不同的是,C#允许为名称空间或者名称空间中的类指定别名:
using TheConsole = System.Console;
public class Hello {
public static void Main() {
TheConsole.WriteLine("使用别名");
}
}
虽然从概念上看,Java的包类似于.NET的名称空间。然而,两者的实现方式不同。在Java中,包的名字同时也是实际存在的实体,它决定了放置.java文件的目录结构。在C#中,物理的包和逻辑的名称之间是完全分离的,也就是说,名称空间的名字不会对物理的打包方式产生任何影响。在C#中,每一个源代码文件可以从属于多个名称空间,而且它可以容纳多个公共类。
.NET中包的实体称为程序集(Assembly)。每一个程序集包含一个manifest结构。manifest列举程序集所包含的文件,控制哪些类型和资源被显露到程序集之外,并把对这些类型和资源的引用映射到包含这些类型与资源的文件。程序集是自包含的,一个程序集可以放置到单一的文件之内,也可以分割成多个文件。.NET的这种封装机制解决了DLL文件所面临的问题,即臭名昭著的DLL Hell问题。
2.10、默认包
在Java中,java.lang包是默认的包,它无需显式导入就已经自动包含。例如,要把一些文本输出到控制台,你可以使用下面的代码:
System.out.println("Hello world from Java");
C#中不存在默认的包。如果要向控制台输出文本,你使用System名称空间Console对象的WriteLine方法。但是,你必须显式导入所有的类。代码如下:
using System;
public class Hello {
public static void Main() {
Console.WriteLine("Hello world from C#");
}
}
2.11、面向对象
Java和C#都是完全面向对象的语言。在面向对象编程的三大原则方面,这两种语言接近得不能再接近。
继承:这两种语言都支持类的单一继承,但类可以实现多个接口。所有类都从一个公共的基类继承。
封装与可见性:无论是在Java还是C#中,你都可以决定类成员是否可见。除了C#的internal访问修饰符之外,两者的可见性机制非常相似。
多态性:Java和C#都支持某些形式的多态性机制,且两者实现方法非常类似。
2.12、可访问性
类的每个成员都有特定类型的可访问性。C#中的访问修饰符与Java中的基本对应,但多出了一个internal。简而言之,C#有5种类型的可访问性,如下所示:
public:成员可以从任何代码访问。
protected:成员只能从派生类访问。
internal:成员只能从同一程序集的内部访问。
protected internal:成员只能从同一程序集内的派生类访问。
private:成员只能在当前类的内部访问。
2.13、派生类
在Java中,我们用关键词“extends”实现继承。C#采用了C++的类派生语法。例如,下面的代码显示了如何派生父类Control从而创建出新类Button:
public class Button: Control { . . }
2.14、最终类
由于C#中不存在final关键词,如果想要某个类不再被派生,你可以使用sealed关键词,如下例所示:
sealed class FinalClass { . . }
2.15、接口
接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。按照惯例,接口的名字以大写字母“I”开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:
interface IShape { void Draw(); }
扩展接口的语法与扩展类的语法一样。例如,下例的IRectangularShape接口扩展IShape接口(即,从IShape接口派生出IRectangularShape接口)。
interface IRectangularShape: IShape { int GetWidth(); }
如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:
interface INewInterface: IParent1, IParent2 { }
然而,与Java不同,C#中的接口不能包含域(Field)。
另外还要注意,在C#中,接口内的所有方法默认都是公用方法。在Java中,方法声明可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。例如,下面的C#接口将产生一个编译错误。
interface IShape { public void Draw(); }
2.16、is和as操作符
C#中的is操作符与Java中的instanceof操作符一样,两者都可以用来测试某个对象的实例是否属于特定的类型。在Java中没有与C#中的as操作符等价的操作符。as操作符与is操作符非常相似,但它更富有“进取心”:如果类型正确的话,as操作符会尝试把被测试的对象引用转换成目标类型;否则,它把变量引用设置成null。
为正确理解as操作符,首先请考虑下面这个例子中is操作符的运用。这个例子包含一个IShape接口,以及两个实现了IShape接口的类Rectangle和Circle。
using System;
interface IShape {
void draw();
}
public class Rectangle: IShape {
public void draw() {
}
public int GetWidth() {
return 6;
}
}
public class Circle: IShape {
public void draw() {
}
public int GetRadius() {
return 5;
}
}
public class LetsDraw {
public static void Main(String[] args) {
IShape shape = null;
if (args[0] == "rectangle") {
shape = new Rectangle();
}
else if (args[0] == "circle") {
shape = new Circle();
}
if (shape is Rectangle) {
Rectangle rectangle = (Rectangle) shape;
Console.WriteLine("Width : " + rectangle.GetWidth());
}
if (shape is Circle) {
Circle circle = (Circle) shape;
Console.WriteLine("Radius : " + circle.GetRadius());
}
}
}
编译好代码之后,用户可以输入“rectangle”或者“circle”作为Main方法的参数。如果用户输入的是“circle”,则shape被实例化成为一个Circle类型的对象;反之,如果用户输入的是“rectangle”,则shape被实例化成为Rectangle类型的对象。随后,程序用is操作符测试shape的变量类型:如果shape是一个矩形,则shape被转换成为Rectangle对象,我们调用它的GetWidth方法;如果shape是一个圆,则shape被转换成为一个Circle对象,我们调用它的GetRadius方法。
如果使用as操作符,则上述代码可以改成如下形式:
using System;
interface IShape {
void draw();
}
public class Rectangle: IShape {
public void draw() {
}
public int GetWidth() {
return 6;
}
}
public class Circle: IShape {
public void draw() {
}
public int GetRadius() {
return 5;
}
}
public class LetsDraw {
public static void Main(String[] args) {
IShape shape = null;
if (args[0] == "rectangle") {
shape = new Rectangle();
}
else if (args[0] == "circle") {
shape = new Circle();
}
Rectangle rectangle = shape as Rectangle;
if (rectangle != null) {
Console.WriteLine("Width : " + rectangle.GetWidth());
}
else {
Circle circle = shape as Circle;
if (circle != null)
Console.WriteLine("Radius : " + circle.GetRadius());
}
}
}
在上面代码的粗体部分中,我们在没有测试shape对象类型的情况下,就用as操作符把shape转换成Rectangle类型的对象。如果shape正好是一个Rectangle,则shape被转换成为Rectangle类型的对象并保存到rectangle变量,然后我们调用它的GetWidth方法。如果这种转换失败,则我们进行第二次尝试。这一次,shape被转换成为Circle类型的对象并保存到circle变量。如果shape确实是一个Circle对象,则circle现在引用了一个Circle对象,我们调用它的GetRadius方法。
2.17、库
C#没有自己的类库。但是,C#共享了.NET的类库。当然,.NET类库也可以用于其他.NET语言,比如VB.NET或者JScript.NET。值得一提的是StringBuilder类,它是对String类的补充。StringBuilder类与Java的StringBuffer类非常相似。
2.18、垃圾收集
C++已经让我们认识到手工管理内存是多么缺乏效率和浪费时间。当你在C++中创建了一个对象,你就必须手工地拆除这个对象。代码越复杂,这个任务也越困难。Java用垃圾收集器来解决这个问题,由垃圾收集器搜集不再使用的对象并释放内存。C#同样采用了这种方法。应该说,如果你也在开发一种新的OOP语言,追随这条道路是一种非常自然的选择。C#仍旧保留了C++的内存手工管理方法,它适合在速度极端重要的场合使用,而在Java中这是不允许的。
2.19、异常处理
如果你听说C#使用与Java相似的异常处理机制,你不会为此而惊讶,对吧?在C#中,所有的异常都从一个名为Exception的类派生(听起来很熟悉?)另外,正如在Java中一样,你还有熟悉的try和catch语句。Exception类属于.NET System名称空间的一部分。
三、Java没有的功能
C#出生在Java成熟之后,因此,C#拥有一些Java(目前)还没有的绝妙功能也就不足为奇。
3.1、枚举器
枚举器即enum类型(Enumerator,或称为计数器),它是一个相关常量的集合。精确地说,enum类型声明为一组相关的符号常量定义了一个类型名字。例如,你可以创建一个名为Fruit(水果)的枚举器,把它作为一个变量值的类型使用,从而把变量可能的取值范围限制为枚举器中出现的值。
public class Demo {
public enum Fruit {
Apple, Banana, Cherry, Durian
}
public void Process(Fruit fruit) {
switch (fruit) {
case Fruit.Apple:
...
break;
case Fruit.Banana:
...
break;
case Fruit.Cherry:
...
break;
case Fruit.Durian:
...
break;
}
}
}
在上例的Process方法中,虽然你可以用int作为myVar变量的类型,但是,使用枚举器Fruit之后,变量的取值范围限制到了Applet、Banana、Cherry和Durian这几个值之内。与int相比,enum的可读性更好,自我说明能力更强。
3.2、结构
结构(Struct)与类很相似。然而,类是作为一种引用类型在堆中创建,而结构是一种值类型,它存储在栈中或者是嵌入式的。因此,只要谨慎运用,结构要比类快。结构可以实现接口,可以象类一样拥有成员,但结构不支持继承。
然而,简单地用结构来取代类可能导致惨重损失。这是因为,结构是以值的方式传递,由于这种传递方式要把值复制到新的位置,所以传递一个“肥胖的”结构需要较大的开销。而对于类,传递的时候只需传递它的引用。
下面是一个结构的例子。注意它与类非常相似,只要把单词“struct”替换成“class”,你就得到了一个类。
struct Point {
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
3.3、属性
C#类除了可以拥有域(Field)之外,它还可以拥有属性(Property)。属性是一个与类或对象关联的命名的特征。属性是域的一种自然扩展——两者都是有类型、有名字的类成员。然而,和域不同的是,属性不表示存储位置;相反,属性拥有存取器(accessor),存取器定义了读取或者写入属性值时必须执行的代码。因此,属性提供了一种把动作和读取、写入对象属性值的操作关联起来的机制,而且它们允许属性值通过计算得到。
在C#中,属性通过属性声明语法定义。属性声明语法的第一部分与域声明很相似,第二部分包括一个set过程和/或一个get过程。例如,在下面的例子中,PropertyDemo类定义了一个Prop属性。
public class PropertyDemo {
private string prop;
public string Prop {
get {
return prop;
}
set {
prop = value;
}
}
}
如果属性既允许读取也允许写入,如PropertyDemo类的Prop属性,则它同时拥有get和set存取过程。当我们读取属性的值时,get存取过程被调用;当我们写入属性值时,set存取过程被调用。在set存取过程中,属性的新值在一个隐含的value参数中给出。
与读取和写入域的方法一样,属性也可以用同样的语法读取和写入。例如,下面的代码实例化了一个PropertyDemo类,然后写入、读取它的Prop属性。
PropertyDemo pd = new PropertyDemo();
pd.Prop = "123"; // set
string s = pd.Prop; // get
3.4、以引用方式传递简单数据类型的参数
在Java中,当你把一个简单数据类型的值作为参数传递给方法时,参数总是以值的方式传递——即,系统将为被调用的方法创建一个参数值的副本。在C#中,你可以用引用的方式传递一个简单数据类型的值。此时,被调用的方法将直接使用传递给它的那个值——也就是说,如果在被调用方法内部修改了参数的值,则原来的变量值也随之改变。
在C#中以引用方式传递值时,我们使用ref关键词。例如,如果编译并运行下面的代码,你将在控制台上看到输出结果16。注意i值被传递给ProcessNumber之后是如何被改变的。
using System;
public class PassByReference {
public static void Main(String[] args) {
int i = 8;
ProcessNumber(ref i);
Console.WriteLine(i);
}
public static void ProcessNumber(ref int j) {
j = 16;
}
}
C#中还有一个允许以引用方式传递参数的关键词out,它与ref相似。但是,使用out时,作为参数传递的变量在传递之前不必具有已知的值。在上例中,如果整数i在传递给ProcessNumber方法之前没有初始化,则代码将出错。如果用out来取代ref,你就可以传递一个未经初始化的值,如下面这个修改后的例子所示。
using System;
public class PassByReference {
public static void Main(String[] args) {
int i;
ProcessNumber(out i);
Console.WriteLine(i);
}
public static void ProcessNumber(out int j) {
j = 16;
}
}
经过修改之后,虽然i值在传递给ProcessNumber方法之前没有初始化,但PassByReference类能够顺利通过编译。
3.5、C#保留了指针
对于那些觉得自己能够恰到好处地运用指针并乐意手工进行内存管理的开发者来说,在C#中,他们仍旧可以用既不安全也不容易使用的“古老的”指针来提高程序的性能。C#提供了支持“不安全”(unsafe)代码的能力,这种代码能够直接操作指针,能够“固定”对象以便临时地阻止垃圾收集器移动对象。无论从开发者还是用户的眼光来看,这种对“不安全”代码的支持其实是一种安全功能。“不安全”的代码必须用unsafe关键词显式地标明,因此开发者不可能在无意之中使用“不安全”的代码。同时,C#编译器又和执行引擎协作,保证了“不安全”的代码不能伪装成为安全代码。
using System;
class UsePointer {
unsafe static void PointerDemo(byte[] arr) {
.
.
}
}
C#中的unsafe代码适合在下列情形下使用:当速度极端重要时,或者当对象需要与现有的软件(比如COM对象或者DLL形式的C代码)交互时。
3.6、代理
代理(delegate)可以看作C++或者其他语言中的函数指针。然而,与函数指针不同的是,C#中的代理是面向对象的、类型安全的、可靠的。而且,函数指针只能用来引用静态函数,但代理既能够引用静态方法,也能够引用实例方法。代理用来封装可调用方法。你可以在类里面编写方法并在该方法上创建代理,此后这个代理就可以被传递到第二个方法。这样,第二个方法就可以调用第一个方法。
代理是从公共基类System.Delegate派生的引用类型。定义和使用代理包括三个步骤:声明,创建实例,调用。代理用delegate声明语法声明。例如,一个不需要参数且没有返回值的代理可以用如下代码声明:
delegate void TheDelegate();
创建代理实例的语法是:使用new关键词,并引用一个实例或类方法,该方法必须符合代理指定的特征。一旦创建了代理的实例,我们就可以用调用方法的语法调用它。
3.7、包装和解除包装
在面向对象的编程语言中,我们通常使用的是对象。但为了提高速度,C#也提供了简单数据类型。因此,C#程序既包含一大堆的对象,又有大量的值。在这种环境下,让这两者协同工作始终是一个不可回避的问题,你必须要有一种让引用和值进行通信的方法。
在C#以及.NET运行时环境中,这个“通信”问题通过包装(Boxing)和解除包装(Unboxing)解决。包装是一种让值类型看起来象引用类型的处理过程。当一个值类型(简单数据类型)被用于一个要求或者可以使用对象的场合时,包装操作自动进行。包装一个value-type值的步骤包括:分配一个对象实例,然后把value-type值复制到对象实例。
解除包装所执行的动作与包装相反,它把一个引用类型转换成值类型。解除包装操作的步骤包括:首先检查并确认对象实例确实是给定value-type的一个经过包装的值,然后从对象实例复制出值。
Java对该问题的处理方式略有不同。Java为每一种简单数据类型提供了一个对应的类封装器。例如,用Integer类封装int类型,用Byte类封装byte类型。
【结束语】本文为你比较了C#和Java。这两种语言很相似,然而,说C#是Java的克隆或许已经大大地言过其实。面向对象、中间语言这类概念并不是什么新东西。如果你准备设计一种面向对象的新语言,而且它必须在一个受管理的安全环境内运行,你难道不会搞出与C#差不多的东西吗? |
posted @ 2006-03-23 11:31 Jingu 阅读(1623) 评论(0)
编辑
"变量"仅仅是数据的存储位置。你可以把数据存放到其中,或者从中取出来作为C#表达式的一部分。变量中所存放的数据的含义是通过类型来控制的。
C#是个强类型(???)的语言。这样,一切对变量的操作都是针对该变量的类型而进行的。为了保证变量中所存放数据的合法性和一致性,对不同类型的变量进行操作有相应的规则。
C#语言的简单类型包含布尔类型和三种数值类型:整型,浮点型和小数。
using System; class Booleans { public static void Main() { bool content = true; bool noContent = false; Console.WriteLine("It is {0} that C# Station provides C# programming language content.", content); Console.WriteLine("The statement above is not {0}.", noContent); } } |
1.在清单1-1中,布尔值作为句子的一部分输出到控制台中。"bool"类型的取值要么为真,要么为假。程序运行结果如下:
>It is True that C# Station provides C# programming language content. >The statement above is not False. |
2.下列表格显示了各种整数类型,所占字节大小和所能表示的数的范围。
| 类型 |
位 |
范围 |
| sbyte |
8 |
-128 to 127 |
| byte |
8 |
0 to 255 |
| short |
16 |
-32768 to 32767 |
| ushort |
16 |
0 to 65535 |
| int |
32 |
-2147483648 to 2147483647 |
| uint |
32 |
0 to 4294967295 |
| long |
64 |
-9223372036854775808 to 9223372036854775807 |
| ulong |
64 |
0 to 18446744073709551615 |
| char |
16 |
0 to 65535 |
在对整数进行计算时,除了字符类型之外,上述这些类型都是适合的。字符类型代表一个Unicode字符。正如在上表中可以看到的,你可以从中选择适合你需要的类型。
3.下列表格显示了单精度类型,双精度类型和小数类型的数据,它们所占的字节,精度和所能表示的数的范围。
| 类型 |
位 |
精度 |
范围 |
| float |
32 |
7 digits |
1.5 x 10-45 to 3.4 x 1038 |
| double |
64 |
15-16 digits |
5.0 x 10-324 to 1.7 x 10308 |
| decimal |
128 |
28-29 decimal places |
1.0 x 10-28 to 7.9 x 1028 |
当你需要进行涉及到分数的操作时,就需要用到实型,然而,对于金融财经方面的数据的计算,小数类型也许是你最好的选择。
4.表达式计算之后可以得出结果。这些表达式把变量和运算符一同放到语句中。下表列出了C#允许的运算符,优先级和结合性。
| 分类 |
运算符 |
结合性 |
| 初级 |
(x) x.y f(x) a[x] x++ x-- new typeof sizeof checked unchecked |
左 |
| 单目 |
+ - ! ~ ++x --x (T)x |
左 |
| 乘法等 |
* / % |
左 |
| 加法等 |
+ - |
左 |
| 移位 |
<< >> |
左 |
| 关系 |
< > <= >= is |
左 |
| 相等 |
== != |
右 |
| 逻辑与 |
& |
左 |
| 逻辑异或 |
^ |
左 |
| 逻辑或 |
| |
左 |
| 条件与 |
&& |
左 |
| 条件或 |
|| |
左 |
| 条件 |
?: |
右 |
| 赋值等 |
= *= /= %= += -= <<= >>= &= ^= |= |
右 |
左结合意味着运算符是从左到右进行运算的。右结合意味着所有的运算是从右到左进行的,如赋值运算符,要等到其右边的计算出来之后,才把结果放到左边的变量中。
| 2.清单 1-2. 单目运算符: Unary.cs |
using System; class Unary { public static void Main() { int unary = 0; int preIncrement; int preDecrement; int postIncrement; int postDecrement; int positive; int negative; sbyte bitNot; bool logNot; preIncrement = ++unary; Console.WriteLine("Pre-Increment: {0}", preIncrement); preDecrement = --unary; Console.WriteLine("Pre-Decrement: {0}", preDecrement); postDecrement = unary--; Console.WriteLine("Post-Decrement: {0}", postDecrement); postIncrement = unary++; Console.WriteLine("Post-Increment: {0}", postIncrement); Console.WriteLine("Final Value of Unary: {0}", unary); positive = -postIncrement; Console.WriteLine("Positive: {0}", positive); negative = +postIncrement; Console.WriteLine("Negative: {0}", negative); bitNot = 0; bitNot = (sbyte)(~bitNot); Console.WriteLine("Bitwise Not: {0}", bitNot); logNot = false; logNot = !logNot; Console.WriteLine("Logical Not: {0}", logNot); } } |
1.当计算表达式的时候,在后置增一和后置减一运算符进行运算时,先返回其值,再进行增一或者减一运算。当使用前置加号和减号运算符进行运算时,是先进行增一或者减一的运算,然后再返回其结果值。
2.在清单1-2中, 变量unary初始化为0,进行++x 运算时,"unary"的值加1,再把其值1赋给"preIncrement"变量。在进行--x运算时,先把"unary"的值减到0, 再把值0赋给"preDecrement"变量。
3.进行x-运算时,先把"unary"的值0赋给"postDecrement" 变量,之后再把"unary"减到-1。进行x++运算时,先把"unary"的值-1赋给"postIncrement"变量,之后再对"unary"加1,使得"unary"变量现在的值为0。
4.变量"bitNot"初始值为0,进行按位取反运算,本例中,数0表示为二进制"00000000",按位取反之后变为-1,其二进制表示为"11111111"。
5.了解一下表达式"(sbyte)(~bitNot)", 任何对类型sbyte, byte, short 或者 ushort 类型数据的运算,返回结果都是整数。要把值赋给bitNot变量,我们必须使用cast (类型)运算符(强制类型转换),其中Type表示你希望转换成的类型(本例中为sbyte)。 Cast运算符把大范围类型的数据转换为小范围类型的数据时,须特别谨慎,因为此时有丢失数据的危险。一般来说,把小类型的数据赋给大类型变量,并没有问题, 因为大范围数据类型的变量具有足够的空间存放小类型数据。 注意在signed 和unsigned类型之间进行Cast运算时,也存在此类危险。 许多初级程序设计教程对变量的位表示作出了很好的讲解,同时也介绍了直接进行Cast运算的危险。
逻辑非(!)运算符可以处理布尔变量值。本例中,"logNot"变量从false 变为true。
上述程序的输出结果如下:
>Pre-Increment: 1 >Pre-Decrement 0 >Post-Decrement: 0 >Post-Increment -1 >Final Value of Unary: 0 >Positive: 1 >Netative: -1 >Bitwise Not: -1 >Logical Not: True |
| 3.清单 1-3. 二元运算符 Binary.cs |
using System; class Binary { public static void Main() { int x, y, result; float floatResult; x = 7; y = 5; result = x+y; Console.WriteLine("x+y: {0}", result); result = x-y; Console.WriteLine("x-y: {0}", result); result = x*y; Console.WriteLine("x*y: {0}", result); result = x/y; Console.WriteLine("x/y: {0}", result); floatResult = (float)x/(float)y; Console.WriteLine("x/y: {0}", floatResult); result = x%y; Console.WriteLine("x%y: {0}", result); result += x; Console.WriteLine("result+=x: {0}", result); } } |
清单1-3 演示了二元操作符的几个例子。加法(+),减法(-),乘法(*)和除法(/)的运算结果,就是我们通常进行的的四则运算的结果。
因为"floatResult"变量是浮点运算类型,所以整型变量"x"和"y" 被强制转换成浮点类型来计算FloatResult。
这里有个求余数的运算符,两个操作数相除,返回余数。
最后一条语句给出了另外一种赋值形式,这里用了(+=)运算符.无论什么时候你使用(+=)运算符,那么这个二进制运算符就应该在运算符左右两边都进行运算,然后把值赋给左边的参数。本语句相当于"result = result + x",并返回同样的值。
前面的课程中,你看到的使用次数较多的一种类型是"string" (字符串)类型。"string"类型是由包含在引号内的Unicode编码的字符构成。例如"This is a string."
另外一种数据类型是数组。数组可以看成是同种类型的元素构成的集合。当声明数组时,你要指定类型名,数组名,维数和数组大小。
| 4.清单 1-4. Array Operations: Array.cs |
using System; class Array { public static void Main() { int[] myInts = { 5, 10, 15 }; bool[][] myBools = new bool[2][]; myBools[0] = new bool[2]; myBools[1] = new bool[1]; double[,] myDoubles = new double[2, 2]; string[] myStrings = new string[3]; Console.WriteLine("myInts[0]: {0}, myInts[1]: {1}, myInts[2]: {2}", myInts[0], myInts[1], myInts[2]); myBools[0][0] = true; myBools[0][1] = false; myBools[1][0] = true;
Console.WriteLine("myBools[0][0]: {0}, myBools[1][0]: {1}", myBools[0][0], myBools[1][0]); myDoubles[0, 0] = 3.147; myDoubles[0, 1] = 7.157; myDoubles[1, 1] = 2.117; myDoubles[1, 0] = 56.00138917; Console.WriteLine("myDoubles[0, 0]: {0}, myDoubles[1, 0]: {1}", myDoubles[0, 0], myDoubles[1, 0]); myStrings[0] = "Joe"; myStrings[1] = "Matt"; myStrings[2] = "Robert"; Console.WriteLine("myStrings[0]: {0}, myStrings[1]: {1}, myStrings[2]: {2}", myStrings[0], myStrings[1], myStrings[2]); } } |
清单 1-4 演示了数组的各种不同实现方法。第一个例子是"myInts"数组,它在声明的同时进行了初始化。
接着是个二维数组,可以把它理解为数组的数组。我们需要使用"new"运算符来实例化初始数组的大小,之后,再对每个子数组使用new运算符。
第三个例子是个二维数组。数组可以是多维的,每一维可以通过逗号隔开,也必须用"new"运算符进行实例化。
最后定义了一个一维的字符串数组。
每种情况下,对于数据元素的访问可以通过引用元素的位置(下标)来进行。数组的大小可以是任何整型值。其下标从0开始。
posted @ 2006-03-23 11:03 Jingu 阅读(297) 评论(0)
编辑
在一个NAMESPACE中,我们也可以用一个别名指代现有的NAMESPACE或是一些其他类型数据。
别名的使用格式如下:
using 别名 = 一个已经存在的类型;
例如:using soholife = System;
下面我们通过几个例子来加深一下理解:
namespace N1.N2
{
class A {}
}
namespace N3
{
using A = N1.N2.A;
class B: A {}
}
这里,在N3中,A是N1.N2.A的别名,而N3.B则继承于N1.N2.A!同样的我们也可以用下面的方式来取得同样的效果:
namespace N3
{
using R = N1.N2;
class B: R.A {}
}
说道这里,我想提个问题,先看下面的例子:
namespace N1.N2
{
class A {}
}
namespace N3
{
class A {}
}
namespace N3
{
using A = N1.N2.A;
}
如果我们这样写,会有问题吗?
答案当然是肯定的了,错!因为一个别名必须是在NAMESPACE中唯一的,而上面由于已经有了
class a{},我们在用using A =N1.NE.A;就所以肯定要出错了!不过如果我们该成:
using B =N1.N2.A;那么结果如何呢?朋友门自己想想吧!我就不多说了!
本来以为可以结束了,突然发现还有一个地方还没有说清楚,可以说是就没有说,呵呵,看来只能晚些回家了,我门还是从问题看起吧:
namespace N1.N2
{
class A {}
}
namespace N3
{
using R = N1.N2;
}
namespace N3
{
class B: R.A {}
}
上面的例子中,不知道大家觉得如何?
如果我来回答的话,错!原来在一个分开的单元中使用别名的时候,别名只是在它所在的单元中(NAMESPACE或其它)可以使用,而在其他单元中是不能够用的,所以上面的那个例子中,在第二个N3中使用R的时候,会提示R未知!当然了,如果我们想使用这样的方式,我们还是有办法的,就是把别名R写到N3的外面:如下
using R = N1.N2;
namespace N3
{
class B: R.A {}
}
好了说了这么多,如果能全部理解的话,我想应该能适当的对NAMESPACE有一些概念了!如果能这样的话,我的目的也达到了!
posted @ 2006-03-23 09:45 Jingu 阅读(629) 评论(0)
编辑