|
|
|

| 名 称: |
C#入门经典(第3版) |
| 编 号: |
2007042512332561 |
| 点击数: |
|
|
商品型号 |
|
商品规格 |
|
| 生 产 商 |
|
品牌商标 |
|
| 商品属性 |
|
推荐等级 |
★★★ |
| 商品类型 |
正常销售商品 |
上架时间 |
2007-4-25 12:29:10 |
| 降价折扣 |
折 |
限购数量 |
本 |
| 优惠期限 |
~ |
| 购物积分 |
0分 |
库存数量 |
本 |
| 赠送点券 |
0点 |
返还现金券 |
0 |
| 促销方案:不促销 |
| 商品价格:市场价:¥98/本 商城价:¥85/本 会员价:¥—/本 优惠价:¥85/本 |
 |
| 商品说明: |

作者:(美)Karli Watson Christian Nagel 等著
出版社:清华大学出版社
出版日期:2006-5-1
ISBN:7302127352
字数:
印次:1
版次:3
纸张:胶版纸
本书将全面介绍C#编程的所有知识,共分为5篇:第1篇是C#语言:介绍了C#语言的所有内容,从基础知识到面向对象的技术,应有尽有。第2篇是Windows编程:介绍如何用C#编写Windows应用程序,如何部署它们。第3篇是Web编程:描述Web应用程序的开发、Web服务和Web应用程序的部署。第4篇是数据访问:介绍在应用程序中如何使用数据,包括存储在硬盘文件上的数据、以XML格式存储的数据和数据库中的数据。第5篇是其他技术:讲述使用C#和.NET Framework的一些额外方式,包括程序集、属性、XML文档、网络和GDI+图形编程。
目 录
第Ⅰ部分 C# 语 言 第1章 C#简介 3 1.1 什么是.NET Framework 3 1.1.1 .NET Framework的内容 4 1.1.2 如何用.NET Framework编写 应用程序 4 1.2 什么是C# 7 1.2.1 用C#能编写什么样的应用程序 7 1.2.2 本书中的C# 8 1.3 Visual Studio 2005 8 1.3.1 Visual Studio 2005 Express产品 8 1.3.2 VS解决方案 9 1.4 小结 9 第2章 编写C#程序 10 2.1 Visual Studio 2005开发环境 10 2.2 控制台应用程序 13 2.2.1 Solution Explorer 15 2.2.2 Properties窗口 16 2.2.3 Error List窗口 16 2.3 Windows Forms应用程序 17 2.4 小结 20 第3章 变量和表达式 22 3.1 C#的基本语法 22 3.2 变量 26 3.2.1 简单类型 26 3.2.2 变量的命名 30 3.2.3 字面值 31 3.2.4 变量的声明和赋值 33 3.3 表达式 34 3.3.1 数学运算符 34 3.3.2 赋值运算符 38 3.3.3 运算符的优先级 39 3.3.4 命名空间 39 3.4 小结 42 3.5 练习 43 第4章 流程控制 44 4.1 布尔逻辑 44 4.1.1 位运算符 46 4.1.2 布尔赋值运算符 50 4.1.3 运算符的优先级更新 51 4.2 goto语句 52 4.3 分支 53 4.3.1 三元运算符 53 4.3.2 if语句 54 4.3.3 switch语句 57 4.4 循环 60 4.4.1 do循环 61 4.4.2 while循环 63 4.4.3 for循环 65 4.4.4 循环的中断 69 4.4.5 无限循环 70 4.5 小结 71 4.6 练习 71 第5章 变量的更多内容 73 5.1 类型转换 73 5.1.1 隐式转换 73 5.1.2 显式转换 75 5.1.3 使用Convert命令进行显式 转换 77 5.2 复杂的变量类型 80 5.2.1 枚举 81 5.2.2 结构 85 5.2.3 数组 87 5.3 字符串的处理 94 5.4 小结 98 5.5 练习 98
第6章 函数 100 6.1 定义和使用函数 101 6.1.1 返回值 102 6.1.2 参数 104 6.2 变量的作用域 111 6.2.1 其他结构中变量的作用域 113 6.2.2 参数和返回值与全局数据 115 6.3 Main()函数 116 6.4 结构函数 119 6.5 函数的重载 120 6.6 委托 121 6.7 小结 124 6.8 练习 124 第7章 调试和错误处理 126 7.1 Visual Studio中的调试 126 7.1.1 非中断(正常)模式下的调试 127 7.1.2 中断模式下的调试 134 7.2 错误处理 143 7.3 小结 149 7.4 练习 150 第8章 面向对象编程简介 151 8.1 什么是面向对象编程 151 8.1.1 什么是对象 152 8.1.2 所有的东西都是对象 154 8.1.3 对象的生命周期 155 8.1.4 静态和实例类成员 156 8.2 OOP技术 156 8.2.1 接口 157 8.2.2 继承 158 8.2.3 多态性 160 8.2.4 对象之间的关系 161 8.2.5 运算符重载 163 8.2.6 事件 163 8.2.7 引用类型和值类型 163 8.3 Windows应用程序中的OOP 164 8.4 小结 166 8.5 练习 167 第9章 定义类 168 9.1 C#中的类定义 168 9.2 System.Object 173 9.3 构造函数和析构函数 174 9.4 Visual Studio 2005中的OOP 工具 178 9.4.1 Class View窗口 178 9.4.2 对象浏览器 181 9.4.3 添加类 182 9.4.4 类图 183 9.5 类库项目 184 9.6 接口和抽象类 187 9.7 结构类型 189 9.8 小结 191 9.9 练习 192 第10章 定义类成员 193 10.1 成员定义 193 10.1.1 定义字段 193 10.1.2 定义方法 194 10.1.3 定义属性 195 10.1.4 在类图中添加成员 200 10.1.5 重制成员 202 10.2 类成员的其他议题 203 10.2.1 隐藏基类方法 203 10.2.2 调用重写或隐藏的基类 方法 205 10.2.3 嵌套的类型定义 206 10.3 接口的实现 207 10.4 部分类定义 210 10.5 示例应用程序 212 10.5.1 规划应用程序 212 10.5.2 编写类库 213 10.5.3 类库的客户应用程序 219 10.6 小结 220 10.7 练习 221 第11章 集合、比较和转换 222 11.1 集合 222 11.1.1 使用集合 223 11.1.2 定义集合 229 11.1.3 索引符 230 11.1.4 给CardLib添加Cards集合 232 11.1.5 关键字值集合和 IDictionary 235 11.1.6 迭代器 236 11.1.7 深度复制 241 11.1.8 给CardLib添加深度复制 243 11.2 比较 245 11.2.1 类型比较 245 11.2.2 值比较 250 11.3 转换 266 11.3.1 重载转换运算符 266 11.3.2 as运算符 267 11.4 小结 268 11.5 练习 269 第12章 泛型 271 12.1 泛型的概念 271 12.2 使用泛型 272 12.2.1 可空类型 272 12.2.2 System.Collections. Generic 命名空间 279 12.3 定义泛型 288 12.3.1 定义泛型类 288 12.3.2 定义泛型接口 300 12.3.3 定义泛型方法 300 12.3.4 定义泛型委托 302 12.4 小结 302 12.5 练习 303 第13章 其他OOP技术 304 13.1 ::运算符和全局命名空间 限定符 304 13.2 定制异常 305 13.3 事件 307 13.3.1 什么是事件 307 13.3.2 使用事件 309 13.3.3 定义事件 311 13.4 扩展和使用CardLib 319 13.5 小结 326 13.6 练习 327 第Ⅱ部分 Windows 编 程 第14章 Windows编程基础 331 14.1 控件 331 14.1.1 属性 332 14.1.2 控件的定位、停靠和对齐 333 14.1.3 事件 334 14.2 Button控件 336 14.2.1 Button控件的属性 337 14.2.2 Button控件的事件 337 14.3 Label和LinkLabel控件 339 14.4 TextBox控件 340 14.4.1 TextBox控件的属性 340 14.4.2 TextBox控件的事件 341 14.5 RadioButton和CheckBox 控件 348 14.5.1 RadioButton控件的属性 349 14.5.2 RadioButton控件的事件 349 14.5.3 CheckBox控件的属性 349 14.5.4 CheckBox控件的事件 350 14.5.5 GroupBox控件 350 14.6 RichTextBox控件 354 14.6.1 RichTextBox控件的属性 354 14.6.2 RichTextBox控件的事件 355 14.7 ListBox和CheckedListBox 控件 360 14.7.1 ListBox控件的属性 360 14.7.2 ListBox控件的方法 361 14.7.3 ListBox控件的事件 362 14.8 ListView控件 365 14.8.1 ListView控件的属性 365 14.8.2 ListView控件的方法 367 14.8.3 ListView控件的事件 367 14.8.4 ListViewItem 368 14.8.5 ColumnHeader 368 14.8.6 ImageList控件 368 14.9 TabControl控件 375 14.9.1 TabControl控件的属性 376 14.9.2 使用TabControl控件 376 14.10 小结 378 14.11 练习 379 第15章 Windows Forms的高级功能 380 15.1 菜单和工具栏 380 15.1.1 两个实质一样的控件 380 15.1.2 使用MenuStrip控件 381 15.1.3 手工创建菜单 381 15.1.4 ToolStripMenuItem控件的 其他属性 384 15.1.5 给菜单添加功能 384 15.2 工具栏 386 15.2.1 ToolStrip控件的属性 387 15.2.2 ToolStrip的项 387 15.2.3 StatusStrip控件 392 15.2.4 StatusStripStatusLabel的 属性 392 15.3 SDI和MDI应用程序 394 15.4 创建控件 403 15.4.1 调试用户控件 409 15.4.2 扩展LabelTextbox控件 410 15.5 小结 412 15.6 练习 413 第16章 使用通用对话框 414 16.1 对话框 414 16.2 如何使用对话框 415 16.3 文件对话框 416 16.3.1 OpenFileDialog 416 16.3.2 SaveFileDialog 427 16.4 打印 432 16.4.1 打印结构 432 16.4.2 打印多个页面 437 16.4.3 PageSetupDialog 439 16.4.4 PrintDialog 442 16.5 打印预览 446 16.5.1 PrintPreviewDialog 446 16.5.2 PrintPreviewControl 446 16.6 FontDialog和ColorDialog 447 16.6.1 FontDialog 447 16.6.2 ColorDialog 449 16.6.3 FolderBrowserDialog 450 16.7 小结 451 16.8 练习 452
第17章 部署Windows应用程序 453 17.1 部署概述 453 17.2 ClickOnce 部署 454 17.3 Visual Studio安装和部署 项目类型 463 17.4 Microsoft Windows安装 程序结构 464 17.4.1 Windows Installer术语 464 17.4.2 Windows Installer的优点 466 17.5 为Simple Editor创建安装 软件包 466 17.5.1 规划安装内容 467 17.5.2 创建项目 467 17.5.3 项目属性 468 17.5.4 安装编辑器 470 17.5.5 File System编辑器 471 17.5.6 File Types编辑器 474 17.5.7 Launch Condition编辑器 475 17.5.8 User Interface编辑器 476 17.6 构建项目 479 17.7 安装 479 17.7.1 Welcome 480 17.7.2 Read Me 481 17.7.3 License Agreement 481 17.7.4 Optional Files 481 17.7.5 选择安装文件夹 482 17.7.6 确认安装 483 17.7.7 进度 483 17.7.8 结束安装 484 17.7.9 运行应用程序 484 17.7.10 卸载 484 17.8 小结 484 17.9 练习 485 第Ⅲ部分 Web 编 程 第18章 Web编程基础 489 18.1 概述 489 18.2 ASP.NET 运行库 490 18.3 创建简单的Web页面 490 18.4 服务器控件 496 18.5 事件处理程序 497 18.6 输入的有效性验证 502 18.7 状态管理 505 18.7.1 客户端的状态管理 506 18.7.2 服务器端的状态管理 508 18.8 身份验证和授权 510 18.8.1 身份验证的配置 511 18.8.2 使用安全控件 515 18.9 读写SQL Server数据库 517 18.10 小结 525 18.11 练习 525 第19章 Web高级编程 526 19.1 示例站点 526 19.2 主页 527 19.3 站点导航 533 19.4 用户控件 535 19.5 个性化配置 537 19.5.1 个性化配置组 538 19.5.2 组件的个性化配置 539 19.5.3 定制数据类型中的个性化 配置 539 19.5.4 匿名用户的个性化配置 540 19.6 Web Parts 541 19.6.1 Web Parts管理器 542 19.6.2 Web Parts区域 542 19.6.3 Editor区域 544 19.6.4 Catalog区域 546 19.6.5 Connections区域 548 19.7 小结 550 19.8 练习 551 第20章 Web服务 552 20.1 Web服务推出之前 552 20.1.1 远程过程调用(RPC) 553 20.1.2 SOAP 553 20.2 使用Web服务的场合 554 20.2.1 宾馆旅行社代理应用程序 554 20.2.2 书籍发布应用程序 555 20.2.3 客户应用程序的类型 555 20.2.4 应用程序的体系结构 555 20.3 Web服务的体系结构 556 20.3.1 Web服务的搜索引擎 557 20.3.2 可以调用的方法 558 20.3.3 调用方法 559 20.3.4 SOAP和防火墙 561 20.3.5 WS-I基本个性化配置 561 20.4 Web服务和.NET Framework 561 20.4.1 创建Web服务 561 20.4.2 客户程序 563 20.5 创建一个简单的ASP.NET Web服务 564 20.5.1 生成的文件 564 20.5.2 添加Web方法 565 20.6 测试Web服务 566 20.7 执行Windows客户程序 567 20.8 异步调用服务 570 20.9 执行ASP.NET客户程序 572 20.10 传送数据 572 20.11 小结 575 20.12 练习 576 第21章 部署Web应用程序 577 21.1 Internet Information Services 577 21.2 IIS配置 578 21.3 复制Web站点 582 21.4 预编译Web站点 583 21.5 Windows安装程序 584 21.5.1 创建安装程序 584 21.5.2 安装Web 应用程序 586 21.6 小结 588 21.7 练习 589 第Ⅳ部分 数 据 访 问 第22章 文件系统数据 593 22.1 流 593 22.2 用于输入和输出的类 594 22.2.1 File和Directory类 595 22.2.2 FileInfo类 596 22.2.3 DirectoryInfo类 597 22.2.4 FileStream对象 598 22.2.5 StreamWriter对象 604 22.2.6 StreamReader对象 606 22.2.7 读写压缩文件 612 22.3 串行化对象 616 22.4 监控文件结构 620 22.5 小结 626 22.6 练习 627 第23章 XML 628 23.1 XML文档 628 23.1.1 XML元素 628 23.1.2 属性 629 23.1.3 XML声明 630 23.1.4 XML文档的结构 630 23.1.5 XML命名空间 631 23.1.6 格式良好并有效的XML 632 23.1.7 验证XML文档 632 23.2 在应用程序中使用XML 637 23.2.1 XML文档对象模型 637 23.2.2 选择节点 646 23.3 小结 654 23.4 练习 654 第24章 数据库和ADO.NET 655 24.1 ADO.NET概述 655 24.1.1 ADO.NET名称的来源 655 24.1.2 ADO.NET的设计目标 656 24.2 ADO.NET类和对象概述 657 24.2.1 提供者对象 658 24.2.2 用户对象 659 24.2.3 使用System.Data命名空间 659 24.3 安装SQL Server和Northwind 示例数据 660 24.3.1 安装SQL Express 661 24.3.2 安装Northwind示例 数据库 661 24.4 用DataReader读取数据 662 24.5 用DataSet读取数据 668 24.5.1 用数据填充DataSet 668 24.5.2 访问DataSet中的表、行 和列 668 24.6 更新数据库 671 24.6.1 给数据库添加行 675 24.6.2 删除行 681 24.7 在DataSet中访问多个表 683 24.7.1 ADO.NET中的关系 683 24.7.2 导航关系 684 24.8 XML和ADO.NET 691 24.9 ADO.NET中的SQL支持 694 24.9.1 DataAdapter对象中的SQL 命令 694 24.9.2 直接执行SQL命令 697 25.9.3 调用SQL存储过程 699 24.10 小结 701 24.11 练习 702 第25章 数据绑定 703 25.1 安装SQL Server和示例数据 703 25.2 创建VS数据库项目 703 25.2.1 数据库对象 706 25.2.2 浏览数据库表和关系 707 25.3 给应用程序添加数据源 709 25.4 添加DataGridView 715 25.4.1 格式化DataGridView 716 25.4.2 添加不同类型的控件 718 25.4.3 查看生成的代码 719 25.4.4 更新数据库 720 25.5 小结 721 25.6 练习 721 第Ⅴ部分 其 他 技 术 第26章 .NET程序集 725 26.1 组件 725 26.1.1 组件的优点 726 26.1.2 组件的简史 726 26.2 .NET程序集的功能 727 26.2.1 自说明性 727 26.2.2 .NET程序集和.NET Framework类库 727 26.2.3 跨语言的程序设计 728 26.2.4 与COM和其他旧代码的 交互操作 728 26.3 程序集的结构 729 26.3.1 查看程序集的内容 731 26.3.2 清单 734 26.3.3 程序集属性 737 26.4 调用程序集 741 26.5 私有和共享程序集 744 26.5.1 私有程序集 744 26.5.2 共享程序集 744 26.5.3 搜索程序集 748 26.6 小结 748 26.7 练习 749 第27章 属性 750 27.1 什么是属性 750 27.2 反射 753 27.3 内置属性 756 27.3.1 System.Diagnostics.Condi tionalAttribute 757 27.3.2 System.ObsoleteAttribute 759 27.3.3 System.SerializableAttribute 760 27.3.4 System.Reflection.Assembly DelaySignAttribute 762 27.4 定制属性 766 27.4.1 TestCaseAttribute 766 27.4.2 System.AttributeUsage Attribute 770 27.4.3 使用属性生成数据库表 776 27.5 小结 790 第28章 XML文档说明 791 28.1 添加XML文档说明 791 28.1.1 XML文档说明的注释 793 28.1.2 使用类图添加XML文档 说明 799 28.1.3 生成XML文档说明文件 802 28.1.4 带有XML文档说明的应用 程序示例 805 28.2 使用XML文档说明 807 28.2.1 编程处理XML文档说明 807 28.2.2 用XSLT格式化XML 文档说明 809 28.2.3 NDoc 810 28.3 小结 811 28.4 练习 811 第29章 网络 812 29.1 联网概述 812 29.1.1 名称的解析 814 29.1.2 统一资源标识符 816 29.1.3 TCP和UDP 817 29.1.4 应用协议 817 29.2 网络编程选项 818 29.3 WebClient 819 29.4 WebRequest和WebResponse 821 29.5 TcpListener和TcpClient 828 29.6 小结 835 29.7 练习 836 第30章 GDI+简介 837 30.1 图形绘制概述 837 30.1.1 Graphics类 838 30.1.2 对象的删除 838 30.1.3 坐标系统 839 30.1.4 颜色 846 30.2 使用Pen类绘制线条 847 30.3 使用Brush 类绘制图形 849 30.4 使用Font 类绘制文本 851 30.5 使用图像进行绘制 855 30.5.1 使用纹理画笔进行绘图 857 30.5.2 使用钢笔绘制图像 858 30.5.3 双倍缓冲 860 30.6 GDI+的高级功能 862 30.6.1 剪切 862 30.6.2 System.Drawing.Drawing2D 863 30.6.3 System.Drawing.Imaging 864 30.7 小结 864 30.8 练习 864
书摘与插图
第1章 C# 简 介 本书的第I部分将介绍使用C# 语言所需的基础知识。第1章将概述C#和.NET Framework、对这些技术的理解、使用它们的原因,以及它们之间的相互关系。 首先讨论一下.NET Framework。这是一种新技术,它包含的许多概念初看起来都不是很容易掌握的(主要因为该架构在应用程序开发环境中引入了一种执行操作的新方式)。也就是说,我们必须在很短的时间里介绍许多新概念,但是,快速浏览这些基础知识对于理解如何利用C#进行编程是非常重要的,所以这是不可避免的。本书的后面将详细论述这里提到的许多论题。 之后,本章将讨论C#本身,包括它的起源和与C++的类似之处。最后,介绍本书使用的主要工具:Visual Studio 2005 (VS)。 本章的主要内容: ● C#和.NET Framework的含义 ● .NET Framework的工作原理和特别之处 ● C#的功能 ● Visual Studio 2005及其在本书的作用 1.1 什么是.NET Framework .NET Framework是Microsoft为开发应用程序而创建的一个富有革命性的新平台。 这句话最有趣的地方是它的含糊不清,但这是有原因的。首先,注意这句话没有说“在Windows操作系统上开发应用程序”。尽管.NET Framework的Microsoft版本运行在Windows操作系统上,但以后将推出运行在其他操作系统上的版本,例如Mono,它是.NET Framework的开发源代码版本(包含一个C#编译器),该版本可以运行在几个操作系统上,包括各种Linux版本和Mac OS。许多这类项目正在开发,在读者阅读本书时可能就已发布了。另外,还可以在个人数字助手(PDA)类设备和一些智能电话上使用Microsoft .NET Compact Framework(基本上是完整 .NET Framework的一个子集)。使用.NET Framework的一个主要原因是它可以作为集成各种操作系统的方式。 另外,上面给出的.NET Framework定义并没有限制应用程序的类型。这是因为本来就没有限制。.NET Framework可以创建Windows应用程序、Web应用程序、Web服务和其他各种类型的应用程序。 .NET Framework的设计方式保证它可以用于各种语言,包括本书要介绍的C#语言,以及C++、Visual Basic、JScript,甚至一些旧的语言,如COBOL。为此,还推出了这些语言的.NET版本,目前还在不断推出更多的.NET版本的语言。所有这些语言都可以访问.NET Framework,它们还可以彼此交互。C#开发人员可以使用Visual Basic程序员编写的代码,反之亦然。 所有这些提供了意想不到的多样性,这也是.NET Framework具有诱人前景的部分原因。 1.1.1 .NET Framework的内容 .NET Framework主要包含一个非常大的代码库,可以在客户语言(如C#)中通过面向对象编程技术(OOP)来使用这些代码。这个库分为不同的模块,这样就可以根据希望得到的结果来选择使用其中的各个部分。例如,一个模块包含Windows应用程序的构件,另一个模块包含联网的代码块,还有一个模块包含Web开发的代码块。一些模块还分为更具体的子模块,例如在Web开发模块中,有用于建立Web服务的子模块。 其目的是,不同的操作系统可以根据自己的特性,支持其中的部分或全部模块。例如,PDA支持所有的核心.NET功能,但不需要某些更深奥的模块。 部分.NET Framework库定义了一些基本类型。类型是数据的一种表达方式,指定其中最基础的部分(例如32位带符号的整数),以便使用.NET Framework在各种语言之间进行交互操作。这称为通用类型系统(Common Type System,CTS)。 除了支持这个库以外,.NET Framework还包含.NET公共语言运行库(Common Language Runtime,CLR),它负责管理用.NET库开发的所有应用程序的执行。 1.1.2 如何用.NET Framework编写应用程序 使用.NET Framework编写应用程序,就是使用.NET代码库编写代码(使用支持Framework的任何一种语言)。本书中所有的示例都使用VS进行开发,VS是一种强大的集成开发环境,支持C#(以及托管和非托管C++、Visual Basic和其他一些语言)。这个环境的优点是便于把.NET功能集成到代码中。我们创建的代码完全是C#代码,但使用.NET Framework,并在需要时利用VS中的其他工具。 为了执行C#代码,必须把它们转换为目标操作系统能够理解的语言,即本机代码,这种转换称为编译代码,由编译器执行。但在.NET Framework下,这个过程分为两个阶段。 1. MSIL和JIT 在编译使用.NET Framework库的代码时,不是立即创建操作系统特定的本机代码,而是把代码编译为Microsoft中间语言(Microsoft Intermediate Language,MSIL)代码,这些代码不专用于任何一种操作系统,也不专用于C#。其他.NET语言,如Visual Basic .NET也可以在第一阶段编译为这种语言,当使用VS开发C#应用程序时,编译过程就由VS完成。 显然,要执行应用程序,必须完成更多的工作,这是Just-In-Time (JIT)编译器的任务,它把MSIL编译为专用于OS和目标机器结构的本机代码。这样OS才能执行应用程序。这里编译器的名称Just-In-Time反映了MSIL仅在需要时才编译的事实。 过去,常常需要把代码编译为几个应用程序,每个应用程序都用于特定的操作系统和CPU结构。这通常是一种优化形式(例如,为了让代码在AMD芯片上运行得更快),但有时是非常重要的(例如对于工作在Win9x 和 WinNT/2000环境下的应用程序)。现在就不必要了,因为顾名思义,JIT编译器使用MSIL代码,而MSIL代码是独立于机器、操作系统和CPU的。目前有几种JIT编译器,每种编译器都用于不同的结构,我们总能找到一个合适的编译器创建所需的本机代码。 这样,用户需要做的工作就比较少了。实际上,可以不考虑与系统相关的细节,把注意力放在代码的功能上就够了。 2. 程序集 在编译应用程序时,所创建的MSIL代码存储在一个程序集中,程序集包括可执行的应用程序文件(这些文件可以直接在Windows上运行,不需要其他程序,其扩展名是.exe)和其他应用程序使用的库(其扩展名是.dll)。 除了包含MSIL外,程序集还包含元信息(即程序集中包含的数据的信息,也称为元数据)和可选的资源(MSIL使用的其他数据,例如声音文件和图片)。元信息允许程序集是完全自我描述的。不需要其他信息就可以使用程序集,也就是说,我们不会遇到下述情形:不能把需要的数据添加到系统注册表中,而这种情形在使用其他平台进行开发时常常出现。 因此,部署应用程序就非常简单了,只需把文件复制到远程计算机上的目录下即可。因为不需要目标系统上的其他信息,所以只需从该目录中运行可执行文件即可(假定安装了.NET CLR)。 当然,不必把运行应用程序所需要的所有信息都安装到一个地方。可以编写一些代码,执行多个应用程序所要求的任务。此时,通常把这些可重用的代码放在所有应用程序都可以访问的地方。在.NET Framework中,这个地方是全局程序集高速缓存(Global Assembly Cache,GAC),把代码放在这个高速缓存中是很简单的,只需把包含代码的程序集放在包含该高速缓存的目录下即可。 3. 托管代码 在把代码编译为MSIL,再用JIT编译器把它编译为本机代码后,CLR的任务还没有全部完成。用.NET Framework编写的代码在执行(这个阶段通常称为运行时(runtime))时是托管的。即CLR管理着应用程序,其方式是管理内存、处理安全性,以及允许进行跨语言调试等。相反,不在CLR控制之下运行的应用程序是非托管的,某些语言如C++可以用于编写这类应用程序,例如,访问操作系统的低级功能。但是,在C#中,只能编写在托管环境下运行的代码。我们将使用CLR的托管功能,让.NET自己与操作系统进行交互。 4. 垃圾回收 托管代码最重要的一个功能是垃圾回收(garbage collection)。这种.NET方法可确保应用程序不再使用某些内存时,这些内存就会被完全释放。在.NET推出以前,这项工作主要由程序员负责,代码中的几个简单错误会把大块内存分配到错误的地方,使这些内存神秘失踪。这通常意味着计算机的速度逐渐减慢,最终导致系统崩溃。 .NET垃圾回收会频繁检查计算机内存,从中删除不再需要的内容。它没有设置时间帧,可能一秒钟内会进行上千次的检查,也可能几秒钟检查一次,或者随时进行检查,但可以肯定进行了检查。 这里要给程序员一些提示。因为这项工作在不可预知的时间进行,所以在设计应用程序时,必须记得要进行这样的检查。需要许多内存才能运行的代码应自己执行这样的检查,而不是坐等垃圾回收,但这不像听起来那样难。 5. 把它们组合在一起 在继续学习之前,先总结一下上述创建.NET应用程序所需要的步骤: (1) 使用某种.NET兼容语言(如C#)编写应用程序代码,如图1-1所示。 (2) 把代码编译为MSIL,存储在程序集中,如图1-2所示。 图 1-1 图 1-2 (3) 在执行代码时(如果这是一个可执行文件,就自动运行,或者在其他代码使用它时运行),首先必须使用JIT编译器将代码编译为本机代码,如图1-3所示。 图 1-3 (4) 在托管的CLR环境下运行本机代码,以及其他应用程序或过程,如图1-4所示。 图 1-4 6. 链接 在上述过程中还有一点要注意。在第(2)步中编译为MSIL的C#代码不一定包含在单独的文件中,可以把应用程序代码放在多个源代码文件中,再把它们编译到一个程序集中。这个过程称为链接,是非常有用的。原因是处理几个较小的文件比处理一个大文件要简单得多。可以把逻辑上相关的代码分解到一个文件中,以便单独处理它,这也更易于在需要代码时找到它们,让开发小组把编程工作分解为可管理的块,让每个人编写一小块代码,而不会破坏已编写好的代码部分或其他人正在处理的部分。 1.2 什么是C# 如上所述,C#是可用于创建要运行在.NET CLR上的应用程序的语言之一,它从C和C++语言演化而来,是Microsoft专门为使用.NET平台而创建的。因为C#是近期发展起来的,所以吸取了以前的教训,考虑了其他语言的许多优点,并解决了它们的问题。 使用C#开发应用程序比使用C++简单,因为其语法比较简单。但是,C#是一种强大的语言,在C++中能完成的任务利用C#也能完成。如前所述,C#中与C++比较高级的功能等价的功能(例如直接访问和处理系统内存),只能在标记为“不安全”的代码中使用。这个高级编程技术是非常危险的(正如它的名称),因为它可能覆盖系统中重要的内存块,导致严重的后果。因此,本书不讨论这个问题。 C#代码常常比C++略长一些。这是因为C#是一种类型安全的语言(与C++不同)。在外行人看来,这表示一旦为某些数据指定了类型,就不能转换为另一个不相关的类型。所以,在类型之间转换时,必须遵守严格的规则。执行相同的任务时,用C#编写的代码通常比C++长。但C#代码更健壮,调试也比较简单,.NET总是可以随时跟踪数据的类型。在C#中,不能完成诸如“把4字节的内存放在这个数据中,使之有10个字节长,并把它解释为X”等的任务,但这并不是一件坏事。 C#只是.NET开发的一种语言,但在我看来,这是最好的一种语言。C#的优点是,它是惟一为.NET Framework设计的语言,是在移植到其他操作系统上的.NET版本中使用的主要语言。要使语言如VB.NET尽可能类似于其以前的语言,且仍遵循CLR,就不能完全支持.NET代码库的某些功能。但C#能使用.NET Framework代码库提供的每种功能。.NET的最新版本还对C#语言进行了几处改进,这是为了满足开发人员的要求,使之更强大。 1.2.1 用C#能编写什么样的应用程序 如前所述,.NET Framework没有限制应用程序的类型。C#使用.NET Framework,所以也没有限制应用程序的类型。这里仅讨论几种常见的应用程序类型。 ● Windows应用程序 这些应用程序如Microsoft Office,有我们很熟悉的Windows外观和操作方式,使用.NET Framework的Windows Forms模块就可以生成这种应用程序。Windows Form模块是一个控件库,其中的控件(例如按钮、工具栏、菜单等)可以用于建立Windows用户界面(UI)。 ● Web应用程序 这些是Web页,可以通过任何Web浏览器查看。.NET Framework包括一个动态生成Web内容的强大系统,允许个性化、实现安全性等。这个系统叫作Active Server Pages.NET (ASP.NET),我们可以使用C#通过Web Forms 创建ASP.NET应用程序。 ● Web服务 这是创建各种分布式应用程序的新方式,使用Web服务可以通过Internet虚拟交换数据。无论使用什么语言创建Web服务,也无论Web服务驻留在什么系统上,都使用一样简单的语法。 这些类型也需要某种形式的数据库访问,这可以通过.NET Framework的Active Data Objects.NET(ADO.NET)部分来实现。也可以使用许多其他资源,例如创建联网组件、输出图形、执行复杂数学任务的工具。 1.2.2 本书中的C# 本书的第Ⅰ部分介绍了C# 语言的语法和用法,但不过分强调.NET Framework。这是必需的,因为我们不能没有一点儿C# 编程基础就使用.NET Framework。首先介绍一些比较简单的内容,把面向对象编程(Object-Oriented Programming,OOP)的问题放在基础知识的后面论述。假定读者没有一点儿编程的知识,这些是首要的规则。 学习了基础知识后,本书还将介绍如何开发上一节列出的应用程序类型。本书的第Ⅱ部分讨论Windows Forms编程,第Ⅲ部分研究Web应用程序和Web服务编程,第Ⅳ部分讲述数据访问(访问数据库、文件系统和XML数据),第Ⅴ部分介绍其他有趣的.NET论题(例如程序集和图形编程)。 1.3 Visual Studio 2005 本书使用Visual Studio 2005 (VS)进行所有的.NET开发,包括简单的命令行应用程序,以及比较复杂的项目类型。VS不是开发C#应用程序所必需的,但使用它可以使任务更简单一些。可以在基本的文本编辑器(例如常见的Notepad)中处理C#源代码文件,再使用命令行应用程序(是.NET Framework的一部分)把代码编译到程序集中。但是,为什么要使用功能全面的VS呢? 下面列出的一些使VS成为.NET开发首选工具的功能。 ● VS可以自动执行编译源代码的步骤,同时可以完全控制重写它们时应使用的任何选项。 ● VS文本编辑器可以配合VS支持的语言(包括C#),这样就可以智能检测错误,在输入代码时给出合适的推荐代码。 ● VS包括Windows Forms 和 Web Forms设计器,允许UI元素的简单拖放设计。 ● 在C#中,许多类型的项目都可以用已有的“模板”代码来创建,不需要从头开始。各种代码文件通常已经为我们准备好了,减少了从头开始一个项目所花的时间。对于新的“Starter Kit”项目类型来说尤其如此,该项目类型可以以功能全面的应用程序为基础进行开发。一些Starter Kit项目类型包含在VS安装程序中,还可以在线使用更多的该项目类型。 ● VS包括几个可自动执行常用任务的向导,它们可以在已有的文件中添加合适的代码,而不需要考虑(在某些情况下)语法的正确性。 ● VS包含许多强大的工具,可以显示和导航项目中的元素,这些元素可以是C#源文件代码,也可以是其他资源,例如位图图像或声音文件。 ● 除了在VS中编写应用程序比较简单外,还可以创建部署项目,以易于为客户提供代码,并方便地安装该项目。 ● 在开发项目时,VS可以使用高级调试技巧,例如能一次调试一行指令,并监视应用程序的状态。 C#还有许多功能,希望读者能掌握它们! 1.3.1 Visual Studio 2005 Express产品 除了Visual Studio 2005之外,Microsoft还提供了几个更简单的开发工具,称为Visual Studio 2005 Express产品。它目前(编写本书时)还是测试版本,但可以在http://lab.msdn.microsoft.com /express上免费获得。 其中两个产品是Visual C# 2005 Express和Visual Web Developer 2005 Express,它们都可以创建几乎所有的C#应用程序。在功能上它们都是VS的删节版本,但外观和操作方式是一样的。尽管它们提供了VS的许多功能,但缺少一些重要的功能,只是我们仍可以在学习本书的过程中使用它们。 1.3.2 VS解决方案 在使用VS开发应用程序时,可以通过创建解决方案来完成。在VS术语中,解决方案不仅仅是一个应用程序,它还包含项目,可以是Windows Forms项目、Web Form项目等。但是,解决方案可以包含多个项目,这样,即使相关的代码最终在硬盘上的多个位置编译为多个程序集,也可以把它们组合到一个地方。 这是非常有用的,因为它可以处理“共享”代码(这些代码放在GAC中),同时,应用程序也使用这段共享代码。在使用惟一的开发环境时,调试代码是非常容易的,因为可以在多个代码块中单步调试指令。 1.4 小结 本章简要介绍了.NET Framework,并讨论了如何轻松地创建各种强大的应用程序。还探讨了把用C#等语言编写的代码转换为可运行的应用程序所需要做的工作,以及使用在.NET Common Language Runtime下运行的托管代码有什么优点。 本章还阐述了C#的实质,以及它与.NET Framework的关系,描述了进行C#开发时所使用的工具——Visual Studio 2005。 本章学习了: ● 什么是.NET Framework,为什么创建它,有哪些因素吸引我们在这个环境中编程 ● 什么是C#,为什么它是在.NET Framework中编程的理想工具 ● 高效开发.NET应用程序需要什么,即像Visual Studio 2005这样的开发环境 第2章介绍如何使用VS运行C#代码,介绍基础知识,并集中讨论C#语言本身,而不是过多地讨论VS的工作原理。 第3章 变量和表达式 要想高效地学习C#的用法,重要的是理解创建计算机程序时需要做什么。计算机程序最基本的描述也许是一系列处理数据的操作,即使是最复杂的示例,这个论述也正确,例如Microsoft Office套装软件之类大型多功能的Windows应用程序。应用程序的用户虽然看不到它们,但这些操作总是在后台上进行。 为了进一步解释它,考虑一下计算机的显示单元。我们常常比较熟悉屏幕上的内容,很难不把它想像为“移动的图片”。但实际上,我们看到的仅是一些数据的显示结果,其最初的形式是存储在计算机内存中的0和1数据流。因此我们在屏幕上进行的任何操作,无论是移动鼠标指针,单击图标,或在字处理器上输入文本,都会改变内存中的数据。 当然,还有一些不太抽象的情形来说明这一点。如果使用计算器应用程序,就要提供数字,对这些数字执行操作,就像用纸和笔计算数字一样,但使用程序会快得多。 如果计算机程序是在对数据执行操作,这说明我们需要某种存储数据的方式,以及处理它们的一些方法。这两种功能是由变量和表达式提供的,本章将探究它们的含义。 在开始之前,应先了解一下C#编程的基本语法,因为我们需要一个环境来学习使用C#语言中的变量和表达式。 本章的主要内容: ● C#的基本语法 ● 变量及其用法 ● 表达式及其用法 3.1 C#的基本语法 C#代码的外观和操作方式与C++和Java非常类似。初看起来,其语法可能比较混乱,不像书面英语和其他语言。但是,在C#编程中,使用的样式是比较清晰的,不用花太多的力气就可以编写出可读性很强的代码。 与其他语言的编译器不同,无论代码中是否有空格、回车符或tab字符(这些字符统称为空白字符),C#编译器都不考虑这些字符。这样格式化代码时就有很大的自由度,但遵循某些规则将有助于使代码易于阅读。 C#代码由一系列语句组成,每个语句都用一个分号来结束。因为空格被忽略,所以一行可以有多个语句,但从可读性的角度来看,通常在分号的后面加上回车符,这样就不能在一行上放置多个语句了。但一句代码放在多个行上是可以的(也比较常见)。 C#是一个块结构的语言,所有的语句都是代码块的一部分。这些块用花括号来界定("{" 和 "}"),代码块可以包含任意多行语句,或者根本不包含语句。注意花括号字符不需要附带分号。 所以,简单的C#代码块如下所示: { ;
; } 其中部分并不是真正的C#代码,而是用这个文本作为C#语句的占位符。注意在这段代码中,第2、3行代码是同一个语句的一部分,因为在第2行的末尾没有分号。 在这个简单的代码块中,还使用了缩进格式,使C#代码的可读性更高。这不是我的发明,而是一个标准规则,实际上在默认情况下VS会自动缩进代码。一般情况下,每个代码块都有自己的缩进级别,即它向右缩进了多少。代码块可以互相嵌套(即块中可以包含其他块),而被嵌套的块要缩进得多一些。 {
; {
;
; }
; } 前面代码的续行通常也要缩进得多一些,如上面第一个示例中的第3行代码。 注释: 在能通过Tools | Options访问的VS Options对话框中,显示了VS用于格式化代码的规则。在Text Editor | C# | Formatting节点的子目录下,包含了完整的格式化规则。此处的大多数设置都反映了还没有讲述的C#部分,但如果以后要修改设置,以更适合自己的个性化样式,就可以回过头来看看这些设置。在本书中,为了简洁起见,所有的代码段都使用默认设置来格式化。 记住,这种样式并不是强制的。但如果不使用它,读者在阅读本书时会很快陷入迷茫之中。 在C#代码中,另一个常见的语句是注释。注释并不是严格意义上的C#代码,但代码最好有注释。注释就是解释,即给代码添加描述性文本(用英语、法语、德语、外蒙古语等),编译器会忽略这些内容。在开始处理比较长的代码段时,注释可用于给正在进行的工作添加提示,例如“这行代码要求用户输入一个数字”,或“这段代码由Bob编写”。C#添加注释的方式有两种。可以在注释的开头和结尾放置标记,也可以使用一个标记,其含义是“这行代码的其余部分是注释”。在C#编译器忽略回车符的规则中,后者是一个例外,但这是一种特殊情况。 要使用第一种方式标记注释,可以在注释的开头加上“/*”,在末尾加上“*/”。这些注释符号可以在单独一行上,也可以在不同的行上,注释符号之间的所有内容都是注释。注释中惟一不能输入的是“*/”,因为它会被看作注释结束标记。所以下面的语句是正确的。 /* This is a comment */
/* And so...
... is this! */ 但下面的语句会产生错误: /* Comments often end with "*/" characters */ 注释结束符号后的内容("*/"后面的字符)会被当作C#代码,因此产生错误。 另一个添加注释的方法是用“//”开始一个注释,其后可以编写任何内容,只要这些内容在一行上即可。下面的语句是正确的: // This is a different sort of comment. 但下面的语句会失败,因为第二行代码会解释为C#代码: // So is this, but this bit isn't. 这类注释可用于语句的说明,因为它们都放在一行上: ; // Explanation of statement 前面说过有两种方法给C#代码添加注释。但在C#中,还有第三类注释,严格地说,这是//语法的扩展。它们都是单行注释,用三个"/"符号来开头,而不是两个。 /// A special comment 在正常情况下,编译器会忽略它们,就像其他注释一样,但可以配置VS,在编译项目时,提取这些注释后面的文本,创建一个特殊格式的文本文件,该文件可用于创建文档说明书。具体内容见第28章。 特别要注意的一点是,C#代码是区分大小写的。与其他语言不同,必须使用正确的大小写形式输入代码,因为简单地用大写字母代替小写字母会中断项目的编译。 如果读者对C#语言没有什么了解,就很难理解这一点,看看下面这行代码,它在第2章的第一个示例中使用: Console.WriteLine("The first app in Beginning C# Programming!"); C#编译器能理解这行代码,因为Console.WriteLine()命令的大小写形式是正确的。但是,下面的语句都不能工作: console.WriteLine("The first app in Beginning C# Programming!"); CONSOLE.WRITELINE("The first app in Beginning C# Programming!"); Console.Writeline("The first app in Beginning C# Programming!"); 这里使用的大小写形式是错误的,所以C#编译器不知道我们要做什么。 幸好,VS在代码的输入方面提供了许多帮助,在大多数情况下,它都知道(程序也知道)我们要做什么。在输入代码的过程中,VS会推荐用户可能要使用的命令,并尽可能纠正大小写问题。 C#控制台应用程序的基本结构 下面看看第2章的控制台应用程序示例(ConsoleApplication1),研究一下它的结构。其代码如下所示:
using System; using System.Collections.Generic; using System.Text;
namespace ConsoleApplication1 { class Program { static void Main(string[] args) { // Output text to the screen. Console.WriteLine("The first app in Beginning C# Programming!"); Console.ReadKey(); } } } 可以立即看出,上一节讨论的所有语法元素这里都有。其中有分号、花括号、注释和适当的缩进。 目前看来,代码中最重要的部分如下所示: static void Main(string[] args) { // Output text to the screen. Console.WriteLine("The first app in Beginning C# Programming!"); Console.ReadKey(); } 在运行控制台应用程序时,就运行这段代码,更准确地说,是运行花括号中的代码块。如前所述,注释行不做任何事情,包含它们只为了简洁而已。其他两行代码在控制台窗口中输出了一些文本,并等待一个响应。但目前我们还不需要关心它的具体机制。 这里要注意一下如何实现上一章介绍的代码突出显示功能,虽然这对于Windows应用程序来说比较重要,但它是一个非常有用的特性。要实现该功能,需要使用#region和#endregion关键字,来定义可以扩展和收缩的代码区域的开头和结尾。例如,可以修改为ConsoleApplication1生成的代码,如下所示: #region Using directives using System; using System.Collections.Generic; using System.Text; #endregion 这样就可以把这些代码行收缩为一行,以后要查看其细节时,可以再次扩展它。这里包含的using语句和其下的namespace语句在本章的后面解释。 注释: 以#开头的任意关键字实际上都是一个预处理指令,严格地说并不是C#关键字。除了这里描述的#region和#endregion关键字之外,其他关键字都相当复杂,用法也比较专业。所以,这是一个读者通读全书后才能探究的主题。 现在不必考虑示例中的其他代码,因为本书前几章仅解释C#的基本语法,至于应用程序进行Console.WriteLine()调用的具体方式,则不在我们的考虑之内。以后会阐述这些代码的重要性。 3.2 变量 如本章的引言所述,变量关系到数据的存储。实际上,可以把计算机内存中的变量看作架子上的盒子。在这些盒子中,可以放入一些东西,再把它们取出来,或者只是看看盒子里是否有东西。变量也是这样,数据可放在变量中,可以从变量中取出数据或查看它们。 尽管计算机中的所有数据都是相同的东西(一组0和1),但变量有不同的内涵,称为类型。下面再使用盒子来类比,盒子有不同的形状和尺寸,某些东西只能放在特定的盒子中。建立这个类型系统的原因是,不同类型的数据需要用不同的方法来处理。变量限定为不同的类型,可以避免混淆它们。例如,组成数字图片的0和1序列与组成声音文件的0和1序列,其处理方式是不同的。 要使用变量,需要声明它们。即给变量指定名称和类型。声明了变量后,就可以把它们用作存储单元,存储声明的数据类型的数据。 声明变量的C#语法是,指定类型和变量名,如下所示: ; 如果使用未声明的变量,代码就不会编译,但此时编译器会告诉我们发生了什么问题,所以这不是一个灾难性错误。另外,使用未赋值的变量也会产生一个错误,编译器会检测出这个错误。 那么我们可以使用什么类型呢? 实际上,可以使用的变量类型是无限多的。其原因是可以自己定义类型,存储各种复杂的数据。 尽管如此,总有一些数据类型是每个人都要使用的,例如存储数值的变量。因此我们应了解一些简单的预定义类型。 3.2.1 简单类型 简单类型就是组成应用程序中基本组成部件的类型,例如数值和布尔值(true或false)。简单类型还可以组成比较复杂的类型。大多数简单类型都是存储数值的,初看起来有点奇怪,肯定只需要一种类型存储数值吗? 数值类型过多的原因是在计算机内存中,把数字作为一系列的0和1来存储的机制。对于整数值,用一定的位(单个数字,可以是0或1)来存储,用二进制格式来表示。以N位来存储的变量可以表示任何介于0 到 (2N - 1)之间的数。大于这个值的数太大,不能存储在这个变量中。 例如,有一个变量存储了2位,在整数和表示该整数的位之间的映射应如下所示: 0 = 00 1 = 01 2 = 10 3 = 11
如果要存储更大的数,就需要更多的位(例如,3位可以存储0~7的数)。 这个论点的结论是要存储每个可以想像得到的数,就需要非常多的位,这并不适合PC。即使可以用足够多的位来表示每一个数,变量使用这些位来存储它,其效率也非常低下,例如,只需要存储从0到10之间的数 (因为存储器被浪费了)。其实4位就足够了,可以用相同的内存空间存储这个范围内的更多数值。 相反,许多不同的整数类型可以用于存储不同范围的数值,占用不同的内存空间(至多64位),其列表如表3-1所示。 表 3-1 类 型 别 名 允 许 的 值 sbyte System.SByte 在 –128~127之间的整数 byte System.Byte 在0~255之间的整数 short System.Int16 在–32768~32767之间的整数 ushort System.UInt16 在0 ~65535之间的整数 int System.Int32 在–2147483648~ 2147483647之间的整数 uint System.UInt32 在0~4294967295之间的整数 long System.Int64 在–9223372036854775808~9223372036854775807之间的整数 ulong System.UInt64 在0~18446744073709551615之间的整数 注意: 这些类型中的每一种都利用了.NET Framework中定义的标准类型。如第1章所述,使用标准类型可以在语言之间交互操作。在C#中这些类型的名称是Framework中定义的别名,表3-1列出了这些类型在.NET Framework库中的名称。 一些变量名称前面的“u”是unsigned的缩写,表示不能在这些类型的变量中存储负号,参见该表中的“允许的值”一列。 当然,除了整数以外,还可以存储浮点数,它们不是整数。可以使用的浮点数变量类型有3种:float, double和 decimal。前两种可以用+/–m×2e的形式存储浮点数,m和e的值随着类型的不同而不同。Decimal使用另一种形式:+/–m×10e。这3种类型、其m和e的值,以及它们在实数中的上下限如表3-2所示。 表 3-2 类 型 别 名 m的最小值 m的最 大值 e的最 小值 e的最 大值 近似的最小值 近似的最大值 float System.Single 0 224 –149 104 1.5 × 10-45 3.4 × 1038 double System.Double 0 253 –1075 970 5.0 × 10-324 1.7 × 10308 decimal System.Decimal 0 296 –26 0 1.0 × 10-28 7.9 × 1028
除了数值类型外,还有另外3种简单类型,如表3-3所示。 表 3-3 类 型 别 名 允 许 的 值 char System.Char 一个Unicode字符,存储0~65535之间的整数 bool System.Boolean 布尔值:true或false string System.String 一组字符
注意组成string的字符数没有上限,因为它可以使用可变大小的内存。 布尔类型bool是C#中最常用的一种变量类型,类似的类型在其他语言的代码中非常丰富。当编写应用程序的逻辑流程时,一个可以是true或false的变量有非常重要的分支作用。例如,考虑一下有多少问题可以用true或false(或yes和no)来回答。执行变量值之间的比较或检查输入的有效性就是后面使用布尔变量的两个编程示例。 介绍了这些类型后,下面用一个小示例来声明和使用它们。在下面的示例中,要使用一些简单的代码声明两个变量,给它们赋值,再输出这些值。 试试看:使用简单类型的变量 (1) 在目录C:\BegVCSharp\Chapter3下创建一个新的控制台应用程序Ch03Ex01。 (2) 给Program.cs添加如下代码: static void Main(string[] args) { int myInteger; string myString; myInteger = 17; myString = "\"myInteger\" is"; Console.WriteLine("{0} {1}.", myString, myInteger); Console.ReadKey(); } (3) 运行代码,结果如图3-1所示。
图 3-1 示例的说明 我们添加的代码完成了3项任务: ● 声明两个变量 ● 给这两个变量赋值 ● 将两个变量的值输出到控制台上 变量声明使用下述代码: int myInteger; string myString;
第一行声明一个类型为int的变量myInteger,第二行声明一个类型为string的变量myString。 提示: 变量的命名是有限制的,不能使用任意的字符序列。本节的后面将介绍命名变量的规则。 接下来的两行代码给变量赋值: myInteger = 17; myString = "\"myInteger\" is"; 使用=赋值运算符(在本章的“表达式”一节中详细介绍)给变量分配两个固定的值(在代码中称为字面值)。把整数值17赋给myInteger,把字符串"myInteger"(包括引号)赋给myString。以这种方式给字符串赋予字面值时,注意必须用双引号把字符串括起来。因此,如果字符串本身包含双引号,就会出现错误,必须用一些表示这些引号字符的其他字符(即转义序列)来替代它们。在本例中,使用序列\"来转义双引号: myString = "\"myInteger\" is"; 如果不使用这些转义序列,而输入如下代码: myString = ""myInteger" is"; 就会出现编译错误。 注意给字符串赋予字面值时,必须小心换行 —— C#编译器会拒绝分布在多行上的字符串字面值。如果要添加一个换行符,可以在字符串中使用回车换行符的转义序列,即\n。例如,赋值语句: myString = "This string has a\nline break."; 会在控制台视图中显示两行,如下所示: This string has a line break. 所有的转义序列都包含一个反斜杠符号,后跟一个字符组合(详见后面的内容),因为反斜杠符号的这种用途,它本身也有一个转义序列,即两个连续的反斜杠\\。 下面继续解释代码,还有一行没有说明: Console.WriteLine("{0} {1}.", myString, myInteger); 它看起来类似于第一个示例中把文本写到控制台上的简单方法,但本例指定了变量。这里不打算详细讨论这行代码。这是本书第一部分用于给控制台窗口输出文本的一种技巧,知道这一点就足够了。在括号中,有两类参数: ● 一个字符串 ● 一个用逗号分隔开的变量列表,这些变量的值将插入到输出字符串中。 输出的字符串是"{0} {1}.",它们并没有包含有用的文本。可以看出,这并不是我们运行代码时希望看到的结果,其原因是:字符串实际上是插入变量内容的一个模板,字符串中的每对花括号都是一个占位符,包含列表中每个变量的内容。每个占位符(或格式字符串)用包含在花括号中的一个整数来表示。整数以0开始,每次递增1,占位符的总数应等于列表中指定的变量数,该列表用逗号分隔开,跟在字符串后。把文本输出到控制台时,每个占位符就会用每个变量的值来替代。在上面的示例中,{0}用第一个变量的值myString替换,{1}用myInteger的内容来替换。 在后面的示例中,就使用这种给控制台输出文本的方式显示代码的输出结果。 最后一行代码在前面的示例中也出现过,用于在程序结束前等待用户输入: Console.ReadKey(); 这里不详细探讨这行代码,但后面的示例会常常用到它。现在只需要知道,它暂停代码的执行,等待用户按下一个键。 3.2.2 变量的命名 如上一节所述,不能把任意序列的字符作为变量名。这并不像第一次听起来那样需要担心什么,因为这种命名系统仍是非常灵活的。 基本的变量命名规则如下: ● 变量名的第一个字符必须是字母、下划线(_)或@。 ● 其后的字符可以是字母、下划线或数字。 另外,有一些关键字对于C#编译器而言有特定的含义,例如前面出现的using和namespace关键字。如果错误地使用其中一个关键字,编译器会产生一个错误,我们马上就会知道出错了,所以不必担心。 例如,下面的变量名是正确的: myBigVar VAR1 _test 下列变量名不正确: 99BottlesOfBeer namespace It's-All-Over 记住,C#是区分大小写的,所以必须小心,不要忘了在声明变量时使用正确的大小写。在程序中引用它们时,即使只有一个字母的大小写形式出错,都不能编译成功。 其进一步的结果是得到多个变量,其名称仅有大小写的区别,例如下面的变量都是不同的: myVariable MyVariable MYVARIABLE 命名约定 变量名是比较常用的,所以有必要用一定的篇幅讨论几种要用到的变量名称。在开始前,要记住这是有争议的。多年以来,出现了不同的系统,一些开发人员拼命维护他们的个人系统。
最近,最流行的系统是所谓的Hungarian记号法。这个系统在所有的变量名上加上一个小写形式的前缀,表示其类型。例如,如果变量的类型是int,就在其名称前加上i(或n),如iAge。使用这个系统,很容易看出各个变量是什么类型的。 更现代的语言如C#灵活地实现了这些系统。与前面介绍的所有类型一样,可以用一两个字母前缀表示变量的类型。但由于可以创建自己的类型,而且在.NET Framework中有上百种更复杂的类型,所以这种系统很快就失效了。在多人完成的项目中,不同的人很容易遇到易混淆的不同前缀,它们可能导致灾难性的后果。 开发人员现在认识到,最好根据变量的作用来命名它们。如果出现问题,就很容易确定变量的类型。在VS中,只需把鼠标指针在变量名上停上足够长的时间,就会弹出一个方框,说明该变量的类型。 目前,在.NET Framework命名空间中有两种命名约定,称为PascalCase和camelCase。在名称中使用的大小写表示它们的用途。它们都应用到由多个单词组成的名称中,并指定名称中的每个单词除了第一个字母大写外,其余字母都是小写。在camelCasing中,还有一个规则,即第一个单词以小写字母开头。 下面是camelCase变量名: age firstName timeOfDeath 下面是PascalCase变量名: Age LastName WinterOfDiscontent Microsoft建议:对于简单的变量,使用camelCase规则,而比较高级的命名则使用PascalCase。 最后,注意许多以前的命名系统常常使用下划线字符作为变量名中各个单词之间的分隔符,例如yet_another_variable。这种用法现在已经淘汰了。 3.2.3 字面值 在前面的示例中,有两个字面值的示例:整数和字符串。其他变量类型也有相关的字面值,如表3-4所示。其中有许多涉及到后缀,即在字面值的后面添加一些字符,指定想要的类型。一些字面值有多种类型,在编译时由编译器根据它们的上下文确定其类型。 表 3-4 类 型 类 别 后 缀 示例/允许的值 bool 布尔 无 true 或 false int, uint, long, ulong 整数 无 100 uint, ulong 整数 u 或 U 100U (续表) 类 型 类 别 后 缀 示例/允许的值 long, ulong 整数 l 或 L 100L ulong 整数 ul, uL, Ul, UL, lu, lU, Lu或 LU 100UL float 实数 f 或 F 1.5F double 实数 无 d或 D 1.5 decimal 实数 m 或 M 1.5M char 字符 无 'a', 或转义序列 string 字符串 无 "a...a",可以包含转义序列 字符串的字面值 在本章的前面,介绍了几个可以在字符串的字面值中使用的转义序列,表3-5是这些转义序列的完整列表,以便以后引用。 表 3-5 转 义 序 列 产生的字符 字符的Unicode值 \' 单引号 0x0027 \" 双引号 0x0022 \\ 反斜杠 0x005C \0 空 0x0000 \a 警告(产生蜂鸣) 0x0007 \b 退格 0x0008 \f 换页 0x000C \n 换行 0x000A \r 回车 0x000D \t 水平制表符 0x0009 \v 垂直制表符 0x000B
表3-5中的“Unicode值”列是字符在Unicode字符集中的16进制值。 与上面一样,使用Unicode转义序列可以指定Unicode字符,该转义序列包括标准的\字符,后跟一个u和一个4位十六进制值(例如,表3-5中x后面的4位数字)。 下面的字符串是等价的: "Karli\'s string." "Karli\u0027s string." 显然,Unicode转义序列还有更多的用途。 也可以逐字地指定字符串,即两个双引号之间的所有字符都包含在字符串中,包括行末字符和需要转义的字符。惟一的例外是双引号字符的转义,它们必须指定,以避免结束字符串。为此,可以在该字符串的前面加一个@字符: @"Verbatim string literal." 这个字符串可以用一般的方式指定,但需要使用下面这种方式: @"A short list: item 1 item 2" 逐字指定的字符串在文件名中非常有用,因为文件名中大量使用了反斜杠字符。如果使用一般的字符串,就必须在字符串中使用2个反斜杠,例如: "C:\\Temp\\MyDir\\MyFile.doc" 而有了逐字指定的字符串字面值,这段代码的可读性就比较高。下面的字符串与上面的等价: @"C:\Temp\MyDir\MyFile.doc" 注意: 从本书的后面可以看出,字符串是引用类型,而本章中的其他类型都是值类型。所以,字符串也可以指定null值,即字符串变量不引用字符串。 3.2.4 变量的声明和赋值 快速回忆一下,前面使用变量的类型和名称来声明它们,例如: int age; 然后用=赋值运算符给变量赋值: age = 25; 注意: 变量在使用前,必须初始化。上面的赋值语句可以用作初始化语句。 这里还可以做两件事,用户可以在C#代码中看到。第一是同时声明多个类型相同的变量,方法是:在类型的后面用逗号分隔变量名,如下所示: int xSize, ySize; 其中xSize和ySize都声明为整数类型。 第二个技巧是在声明变量的同时为它们赋值,即把两行代码合并在一起: int age = 25; 可以同时使用这两个技巧: int xSize = 4, ySize = 5; xSize 和 ySize被赋予不同的值: 注意下面的代码: int xSize, ySize = 5; 其结果是ySize被初始化,而xSize仅进行了声明,在使用前仍需要初始化。 3.3 表达式 前面介绍了如何声明和初始化变量,下面该处理它们了。C#包含许多进行这类处理的运算符,包括前面已经使用过的=赋值运算符,把变量和字面值(在使用运算符时,它们都称为操作数)与运算符组合起来,就可以创建表达式,它是计算的基本建立块。 运算符的范围非常广泛,有简单的,也有非常复杂的,其中一些可能只在数学应用程序中使用。简单的操作包括所有的基本数学操作,例如+运算符是把两个操作数加在一起,而复杂的操作则包括通过变量内容的二进制表示来处理它们。还有专门用于处理布尔值的逻辑运算符,和赋值运算符=。 本章主要介绍数学和赋值运算符,而逻辑运算符在第4章中介绍,主要论述控制程序流程的布尔逻辑。 运算符大致分为3类。 ● 一元运算符,处理一个操作数 ● 二元运算符,处理两个操作数 ● 三元运算符,处理三个操作数 大多数运算符都是二元运算符,只有几个一元运算符和一个三元运算符,即条件运算符(条件运算符是一个逻辑运算符,它返回一个布尔值,详见第4章)。 下面先介绍数学运算符,它包括一元运算符和二元运算符。 3.3.1 数学运算符 有5个简单的数字运算符,其中2个有二元和一元两种形式。表3-6列出了这些运算符,并用一个小示例来说明它们的用法,以及使用简单的数值类型(整数和浮点数)时它们的结果。 表 3-6 运 算 符 类 别 示例表达式 结 果 + 二元 var1 = var2 + var3; var1的值是var2与var3的和 – 二元 var1 = var2–var3; var1是从var2的值减去var3的值所得的值 * 二元 var1 = var2 * var3; var1 的值是var2与var3的乘积 / 二元 var1 = var2 / var3; var1是var2除以var3所得的值 % 二元 var1 = var2 % var3; var1是var2除以var3所得的余数 + 一元 var1 = +var2; var1的值等于var2的值 – 一元 var1 =–var2; var1的值等于var2的值除乘以–1 注释: +(一元)运算符有点古怪,因为它对结果没有影响。它不会把值变成正的:如果var2是-1,则+var2仍是-1。但是,这是一个普遍认可的运算符,所以也把它包含进来。这个运算符最有用的方面是,可以定制它的操作,本书在后面探讨运算符的重载时会介绍它。 上面的示例都使用简单的数值类型,因为使用其他简单类型,结果可能不太清晰。如果把两个布尔值加在一起,会得到什么结果?此时,如果对bool变量使用+(或其他数学运算符),编译器会报告出错。char变量的相加也会有点让人摸不着头脑。记住,char变量实际上存储的是数字,所以把两个char变量加在一起也会得到一个数字(其类型为int)。这是一个隐式转换的示例,稍后将详细介绍这个主题和显式转换,因为它也可以应用到var1、var2和 var3都是混合类型的情况。 如前所述,二元运算符+在用于字符串类型变量时也是有意义的。此时,表3-7的表项应如下所示。 表 3-7 运 算 符 类 别 示例表达式 结 果 + 二元 var1 = var2 + var3; var1的值是存储在var2和var3中的字符串的连接值
但其他数学运算符不能用于字符串的处理。 这里应介绍的另外两个运算符是递增和递减运算符,它们都是一元运算符,可以以两种方式使用:放在操作数的前面或后面。简单表达式的结果如表3-8所示。 表 3-8 运 算 符 类 别 示例表达式 结 果 ++ 一元 var1 = ++var2; var1的值是var2 + 1,var2递增1 – – 一元 var1 = – – var2; var1的值是var2 – 1,var2 递减1 ++ 一元 var1 = var2++; var1的值是var2,var2递增1 – – 一元 var1 = var2– – ; var1的值是var2,var2 递减1
这里的关键因素是这些运算符总是改变存储在操作数中的值。 ● ++总是使操作数加1 ● – – 总是使操作数减1 var1中存储的结果有区别,其原因是运算符的位置决定了它什么时候发挥作用。把运算符放在操作数的前面,则操作数是在进行任何其他计算前受到运算符的影响,而把运算符放在操作数的后面,则操作数是在完成表达式的计算后受到运算符的影响。 这有益于另一个示例,考虑下面的代码: int var1, var2 = 5, var3 = 6; var1 = var2++ *––var3; 问题是,要把什么值赋予var1?在表达式计算前,var3前面的运算符––会起作用,把它的值从6改为5。可以忽略var2后面的++运算符,因为它是在计算完成后才发挥作用,所以var1的结果是5与5的乘积,即25。
在许多情况下,这些简单的一元运算符使用起来非常方便,它们实际上是下述表达式的简写形式: var1 = var1 + 1; 这类表达式有许多用途,特别适合于在循环中使用,这将在第4章讲述。 下面介绍一个示例,说明如何使用数学运算符,并介绍另外两个有用的概念。代码提示用户输入一个字符串和两个数字,然后显示计算结果。 试试看:用数学运算符处理变量 (1) 在目录C:\BegVCSharp\Chapter3下创建一个新控制台应用程序Ch03Ex02。 (2) 在Program.cs中添加如下代码: static void Main(string[] args) { double firstNumber, secondNumber; string userName; Console.WriteLine("Enter your name:"); userName = Console.ReadLine(); Console.WriteLine("Welcome {0}!", userName); Console.WriteLine("Now give me a number:"); firstNumber = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("Now give me another number:"); secondNumber = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("The sum of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber + secondNumber); Console.WriteLine("The result of subtracting {0} from {1} is {2}.", secondNumber, firstNumber, firstNumber - secondNumber); Console.WriteLine("The product of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber * secondNumber); Console.WriteLine("The result of dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber / secondNumber); Console.WriteLine("The remainder after dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber % secondNumber); Console.ReadKey(); } (3) 执行代码,结果如图3-2所示。
图 3-2 (4) 输入名称,按下回车键,如图3-3所示。
图 3-3 (5) 输入一个数字,按下回车键,再输入另一个数字,按下回车键,如图3-4所示。
图 3-4 示例的说明 除了演示数学运算符外,这段代码还引入了两个重要的概念,在以后的示例中将多次用到这些概念。 ● 用户输入 ● 类型转换 用户输入使用与前面Console.WriteLine()命令类似的语法。但这里使用Console.ReadLine()。这个命令提示用户输入信息,并把它们存储在string变量中。 string userName; Console.WriteLine("Enter your name:"); userName = Console.ReadLine(); Console.WriteLine("Welcome {0}!", userName); 这段代码把已赋值变量userName的内容写到屏幕上。 这个示例还读取了两个数字,下面略微展开讨论一下。因为Console.ReadLine()命令生成一个字符串,而我们希望得到一个数字,这就引入了类型转换的问题。第5章将详细讨论类型转换,下面先看看本例使用的代码。 首先,声明要存储数字的变量: double firstNumber, secondNumber; 接着,给出提示,对Console.ReadLine()得到的字符串使用命令Convert.ToDouble(),把字符串转换为double类型,把这个数值赋给前面声明的变量firstNumber: Console.WriteLine("Now give me a number:"); firstNumber = Convert.ToDouble(Console.ReadLine()); 这个语法是相当简单的,其他的许多转换也用这种方式进行。 其余的代码以相同的方式获取第二个数: Console.WriteLine("Now give me another number:"); secondNumber = Convert.ToDouble(Console.ReadLine()); 然后输出两个数字的加、减、乘、除的结果,并使用余数运算符(%)显示除操作的余数。 Console.WriteLine("The sum of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber + secondNumber); Console.WriteLine("The result of subtracting {0} from {1} is {2}.", secondNumber, firstNumber, firstNumber - secondNumber); Console.WriteLine("The product of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber * secondNumber); Console.WriteLine("The result of dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber / secondNumber); Console.WriteLine("The remainder after dividing {0} by {1} is {2}.", firstNumber, secondNumber, firstNumber % secondNumber); 注意我们提供了表达式firstNumber + secondNumber等,作为Console.WriteLine()语句的一个参数,而没有使用中间变量: Console.WriteLine("The sum of {0} and {1} is {2}.", firstNumber, secondNumber, firstNumber + secondNumber); 这种语法可以使代码的可读性比较好,减少需要编写的代码量。 3.3.2 赋值运算符 直到现在,我们一直在使用简单的=赋值运算符,其实还有其他赋值运算符,而且它们都非常有用。 除了=运算符外,其他赋值运算符都以类似的方式工作。与=一样,它们都是根据运算符和右边的操作数,把一个值赋给左边的变量。 与以前一样,用表格的方式列出这些运算符及其说明,如表3-9所示。 表 3-9 运 算 符 类 别 示例表达式 结 果 = 二元 var1 = var2; var1被赋予var2的值 += 二元 var1 += var2; var1被赋予var1与var2的和 –= 二元 var1–= var2; var1被赋予var1与var2的差 *= 二元 var1 *= var2; var1被赋予var1与var2的乘积 /= 二元 var1 /= var2; var1被赋予var1与var2相除所得的结果 %= 二元 var1 %= var2; var1被赋予var1与var2相除所得的余数
可以看出,这些运算符把var1也包括在计算过程中,下面的代码: var1 += var2; 与下面的代码结果相同。 var1 = var1 + var2; 注意: +=运算符也可以用于字符串,与+运算符一样。 使用这些运算符,特别是在使用长变量名时,可以使代码更容易阅读。 3.3.3 运算符的优先级 在计算表达式时,每个运算符都会按顺序处理。但这并不意味着从左至右地运用这些运算符。 例如,有下面的代码: var1 = var2 + var3; 其中+运算符就是在=运算符之前进行计算的。 在其他一些情况下,运算符的优先级并没有这么明显,例如: var1 = var2 + var3 * var4; 其中*运算符先计算,其后是+运算符,最后是=运算符,这是标准的数学顺序,其结果与我们在纸上进行算术运算的结果相同。 像这样的计算,可以使用括号控制运算符的优先级,例如: var1 = (var2 + var3) * var4; 括号中的内容先计算,即+运算符在*运算符之前计算。 对于前面介绍的运算符,其优先级如表3-10所示,优先级相同的运算符(如*和/)按照从左至右的顺序计算。 表 3-10 优 先 级 运 算 符 优先级由高到低 ++, – – (用作前缀); +,–(一元) *, /, % +,– =, *=, /=, %=, +=,–= ++, – – (用作后缀) 注意: 括号可用于忽略优先级顺序,如上所述。 3.3.4 命名空间 在继续学习前,应花一定的时间了解一个比较重要的主题—— 命名空间。它们是.NET中提供应用程序代码容器的方式,这样就可以惟一地标识代码及其内容。命名空间也用作.NET Framework中给项分类的一种方式。大多数项都是类型定义,例如本章描述的简单类型(System.Int32等)。 在默认情况下,C#代码包含在全局命名空间中。这意味着对于包含在这段代码中的项,只要按照名称进行引用,就可以由全局命名空间中的其他代码访问它们。可以使用namespace关键字为花括号中的代码块显式定义命名空间。如果在该命名空间代码的外部使用命名空间中的名称,就必须写出该命名空间中的限定名称。 限定名称包括它所有的继承信息。基本上,这意味着,如果一个命名空间中的代码需要使用在另一个命名空间中定义的名称,就必须包括对该命名空间的引用。限定名称在不同的命名空间级别之间使用句点字符(.)。 例如: namespace LevelOne { // code in LevelOne namespace
// name "NameOne" defined }
// code in global namespace 这段代码定义了一个命名空间LevelOne,以及该命名空间中的一个名称NameOne(注意这里没有列出其他代码,是为了使我们的讨论更具普遍性,并在定义命名空间的地方添加了一个注释)。在命名空间LevelOne中编写的代码可以使用NameOne来引用该名称,不需要任何分类信息。但全局命名空间中的代码必须使用分类名称LevelOne.NameOne来引用这个名称。 在命名空间中,使用关键字namespace还可以定义嵌套的命名空间。嵌套的命名空间通过其层次结构来引用,并使用句点区分层次结构的层次。这最好用一个示例来说明。考虑下面的命名空间: namespace LevelOne { // code in LevelOne namespace
namespace LevelTwo { // code in LevelOne.LevelTwo namespace
// name "NameTwo" defined } }
// code in global namespace 在全局命名空间中,NameTwo必须引用为LevelOne.LevelTwo.NameTwo,在LevelOne命名空间中,则可以引用为LevelTwo.NameTwo,在LevelOne.LevelTwo命名空间中,则可以引用为NameTwo。 要注意的是,名称是由命名空间惟一定义的。可以在LevelOne 和 LevelTwo命名空间中定义名称NameThree: namespace LevelOne { // name "NameThree" defined
namespace LevelTwo { // name "NameThree" defined } } 这定义了两个不同的名称LevelOne.NameThree和LevelOne.LevelTwo.NameThree,可以独立使用它们,互不干扰。 创建了命名空间后,就可以使用using语句简化对它们包含的名称的访问。实际上,using语句的意思是“我们需要这个命名空间中的名称,所以不要每次总是要求对它们分类”。例如,在下面的代码中,LevelOne命名空间中的代码可以访问LevelOne.LevelTwo命名空间中的名称,而无需分类: namespace LevelOne { using LevelTwo;
namespace LevelTwo { // name "NameTwo" defined } } LevelOne命名空间中的代码现在可以直接使用NameTwo引用LevelTwo.NameTwo。 有时,与上面的NameThree示例一样,不同命名空间中的相同名称会产生冲突,使系统崩溃(此时,代码是不能编译的,编译器会告诉我们名称有冲突)。此时,可以为命名空间提供一个别名,作为using语句的一部分。 namespace LevelOne { using LT = LevelTwo;
// name "NameThree" defined
namespace LevelTwo { // name "NameThree" defined } } LevelOne命名空间中的代码可以把LevelOne.NameThree引用为NameThree,把LevelOne. LevelTwo.NameThree引用为LT.NameThree。 using语句可以应用到包含它们的命名空间,以及该命名空间中包含的嵌套命名空间中。在上面的代码中,全局命名空间不能使用LT.NameThree。但如果Using语句声明如下: using LT = LevelOne.LevelTwo;
namespace LevelOne { // name "NameThree" defined
namespace LevelTwo { // name "NameThree" defined } } 这样全局命名空间中的代码和LevelOne命名空间就可以使用LT.NameThree。 这里有一点要注意:using语句本身不能访问另一个命名空间中的名称。除非命名空间中的代码以某种方式链接到项目上,或者代码是在该项目的源文件中定义的,或在链接到该项目的其他代码中定义的,否则就不能访问其中包含的名称。另外,如果包含命名空间的代码链接到项目上,无论是否使用using,都可以访问其中包含的名称。Using语句便于我们访问这些名称,减少代码量,使之更合理。 回过头来看看本章开头的ConsoleApplication1中的代码,下面的代码被应用到命名空间上: #region Using directives
using System; using System.Collections.Generic; using System.Text;
#endregion
namespace ConsoleApplication1 { ... } Using指令块中的3行代码使用using声明:在这段C#代码中使用System、System. Collections.Generic和System.Text命名空间,它们可以在该文件的所有命名空间中访问,无需分类。System命名空间是.NET Framework应用程序的根命名空间,包含控制台应用程序所需要的所有基本功能。其他两个命名空间常常用于控制台应用程序,所以该程序包含了这3行代码。 最后,为应用程序代码本身声明一个命名空间ConsoleApplication1。 3.4 小结 本章介绍了创建有效C#应用程序的许多基础知识,讲述了C#的基本语法,分析了在创建控制台应用程序项目时VS生成的基本控制台应用程序代码。 本章的主要内容是变量的使用。我们描述了变量,阐述了如何创建变量,如何给它们赋值,如何处理它们以及它们包含的值。同时,介绍了一些基本的用户交互,描述了如何把文本输出到控制台应用程序上,如何读取用户的输入。这涉及到一些非常基本的类型转换。类型转换是一个复杂的问题,将在第5章详细论述。 本章还介绍了如何把运算符和操作数组合为表达式,并说明了这些运算符的执行方式,以及执行它们的顺序。 最后介绍了命名空间。随着本书的介绍的深入,命名空间会显得越来越重要。这里仅以比较抽象的方式介绍了这个主题,完整的论述见后面的内容。 本章学习了: ● C#的基本语法 ● 在创建控制台应用程序项目时VS所做的工作 ● 变量的理解和使用 ● 表达式的理解和使用 ● 命名空间的含义 到目前为止,所有的编程工作都是逐行完成的。第4章将学习如何使用循环技术和条件分支控制程序执行的流程,使代码的效率更高。 3.5 练习 (1) 在下面的代码中,如何引用命名空间fabulous中的名称great? namespace fabulous { // code in fabulous namespace }
namespace super { namespace smashing { // great name defined } } (2) 下面哪些不是合法的变量名? ● myVariableIsGood ● 99Flake ● _floor ● time2GetJiggyWidIt ● wrox.com (3) 字符串supercalifragilisticexpialidocious是因为太长了而不能放在string变量中吗?为什么? (4) 考虑运算符的优先级,列出下述表达式的计算步骤。 resultVar += var1 * var2 + var3 % var4 / var5; (5) 编写一个控制台应用程序,要求用户输入4个int值,并显示它们的乘积。提示:可以考虑使用Convert.ToDouble()命令,该命令可以把用户在控制台上输入的数转换为double;从string转换为int的命令是Convert.ToInt32()。
第6章 函 数 到目前为止,我们所看到的代码都是以单个代码块的形式出现的,其中有一些重复执行的循环代码,和有条件地执行的分支语句。如果要对数据执行某种操作,就应把所需要的代码放在合适的地方。 这种代码结构的作用是有限的。某些任务常常需要在一个程序中执行好几次,例如查找数组中的最大值。此时可以把相同(或几乎相同)的代码块按照需要放在应用程序中,但这样做也会有问题。在某个常见任务中,即使进行非常小的改动(例如,修改某个代码错误),也需要修改多个代码块,这些代码块可能分布在整个应用程序中。如果忘了修改其中的一个代码块就会产生很大的影响,导致整个应用程序失败。另外,应用程序也比较长。 这个问题的解决方法是使用函数。在C#中,函数是一种方法,可提供在应用程序中的任何一处执行的代码块。 提示: 本章介绍的特定类型的函数称为方法。但是,这个术语在.NET编程中有非常特殊的含义,本书后面会详细讨论它,所以现在不使用这个术语。 例如,有一个函数返回数组中的最大值,可以在代码的任何位置使用这个函数,且在每个地方都使用相同的代码行。因为只需要提供一次这段代码,所以对代码的任何修改都只影响使用该函数进行的计算。这个函数可以看作包含可重用的代码。 函数还可以使代码的可读性更高,因为可以使用函数把相关的代码组合在一起。如果这么做,应用程序主体就会非常短,因为代码的内部工作部分被分散了。这类似于在VS中使用大纲视图把代码分解为各个区域,应用程序就会得到更富有逻辑的结构。 函数还可以用于创建多用途的代码,让它们对不同的数据执行相同的操作。可以以参数的形式为一个函数提供信息,以返回值的形式得到函数的结果。在上面的示例中,参数就是一个要搜索的数组,而返回值就是数组中的最大值。这意味着每次可以使用同一个函数处理不同的数组。函数的参数和返回值共同定义了函数的签名。 本章的主要内容: ● 定义和使用不接受或返回任何数据的简单函数。 ● 介绍在函数之间传送数据的方式。 ● 讨论变量作用域的问题,这涉及到C#应用程序中如何把数据定位到特定代码区域,在把代码分解到多个函数中时,这个问题特别重要。 ● 深入探讨C#应用程序中的一个重要函数:Main()。论述如何使用这个函数的内置功能来利用命令行参数,以便在运行应用程序时,把信息传送给它们。 ● 讨论第5章介绍的结构类型的另一个特性,即可以把函数作为结构类型的成员。 最后探讨两个比较高级的主题:函数的重载和委托。 ● 函数重载技术可以提供名称相同但签名不同的多个函数。 ● 委托是一种变量类型,可以间接地使用函数。同一个委托可调用匹配于特定签名的任何函数,在运行期间在几个函数之间进行选择。 6.1 定义和使用函数 本节介绍如何把函数添加到应用程序中,以及如何在代码中使用(调用)它们。首先从基础知识开始,看看不交换任何数据的简单函数以及调用它们的代码,然后介绍更高级的函数用法。 首先看一个示例。 试试看:定义和使用基本函数 (1) 在目录C:\BegVCSharp\Chapter6下创建一个新控制台应用程序Ch06Ex01。 (2) 把下述代码添加到Program.cs中: class Program { static void Write() { Console.WriteLine("Text output from function."); }
static void Main(string[] args) { Write(); Console.ReadKey(); } } (3) 执行代码,结果如图6-1所示。
图 6-1 示例的说明 下面的4行代码定义了函数Write(): static void Write() { Console.WriteLine("Text output from function."); } 这些代码把一些文本输出到控制台窗口中。但此时这些并不重要,我们更关心定义和使用函数的机制。
函数定义由以下几部分组成: ● 两个关键字:static和void ● 函数名后跟圆括号,如Write() ● 一个要执行的代码块,放在花括号中 注意: 函数名一般采用PascalCasing形式来编写。 定义Write()函数的代码非常类似于应用程序中的其他代码: static void Main(string[] args) { ... } 这是因为,到目前为止我们编写的所有代码(除了类型定义之外)都是函数的一部分。函数Main()是(如自动生成的代码所述)控制台应用程序的入口点函数。当执行一个C#应用程序时,就会调用它包含的入口点函数,这个函数执行完后,应用程序就终止了。所有的C#可执行代码都必须有一个入口点。 Main()函数和Write()函数的惟一区别(除了它们包含的代码)是函数名Main后面的圆括号中还有一些代码,这是指定参数的方式,详见后面的内容。 如上所述,Main()函数和Write()函数都是使用关键字static和void定义的。关键字static与面向对象的概念相关,本书在后面讨论。现在只需记住,在本节的应用程序中所使用的所有函数都必须使用这个关键字。 而void解释起来就要简单得多。这个关键字表明函数没有返回值。在本章的后面,将讨论函数有返回值时需要编写什么代码。 继续下去,调用函数的代码如下所示: Write(); 输入函数名,后跟空括号即可。在程序执行到这行代码时,就会运行Write()函数中的代码。 注意: 在定义函数和调用函数时,必须使用圆括号。如果删除它们,代码就不能编译。 6.1.1 返回值 通过函数进行数据交换的最简单方式是利用返回值。有返回值的函数会计算这个值,其方式与在表达式中使用变量计算它们包含的值完全相同。与变量一样,返回值也有数据类型。 例如,有一个函数getString(),其返回值是一个字符串,可以在代码中使用该函数,如下所示: string myString; myString = getString(); 另外,还有一个函数getVal(),它返回一个double值,可以在数学表达式中使用它。 double myVal; double multipler = 5.3; myVal = getVal() * multiplier; 当函数返回一个值时,可以用下面两种方式修改函数: ● 在函数声明中指定返回值的类型,但不使用关键字void。 ● 使用return关键字结束函数的执行,把返回值传送给调用代码。 在代码的术语中,控制台应用程序中的下述代码看起来像是前面见过的函数类型: static () { ... return ; } 这里惟一的限制是必须是一个值,其类型可以是,也可以隐式转换为该类型。但是,可以是任何类型,包括前面介绍的较复杂的类型。 这段代码可以很简单: static double getVal() { return 3.2; } 但是,返回值通常是函数执行的一些处理的结果,如上所示,返回值是使用const变量得到的。 在执行到return语句时,程序会立即返回调用代码。这个语句后面的代码都不会执行。但是,这并不意味着return语句只能放在函数体的最后一行。可以在前边的代码里使用return,也可能在执行了分支逻辑之后使用。把return语句放在for循环中、if块中,或其他结构中,会使该结构立即终止,函数也立即终止。例如: static double getVal() { double checkVal; // checkVal assigned a value through some logic. if (checkVal < 5) return 4.7;
return 3.2; } 根据checkVal的值,将返回两个值中的一个。 这里惟一的限制是return语句必须在函数的闭合花括号 } 之前处理。下面的代码是不合法的: static double getVal() { double checkVal; // checkVal assigned a value through some logic. if (checkVal < 5)
return 4.7; } 如果 checkVal>= 5,就不会执行到 return语句,这是不允许的。所有的处理路径都必须执行到 return语句。在大多数情况下,编译器会检查是否执行到 return语句,如果没有,就给出一个错误“并不是所有的处理路径都返回一个值”。 最后要注意的是,return可以用在通过void关键字声明的函数中(没有返回值)。如果这么做,函数就会立即终止。以这种方式使用return语句时,在return关键字和其后的分号之间提供返回值是错误的。 6.1.2 参数 当函数接受参数时,就必须指定下述内容: ● 函数在其定义中指定接受的参数列表,以及这些参数的类型。 ● 在每个函数调用中匹配的参数列表。 这涉及到下述代码: static ( , ...) { ... return ; } 其中可以有任意多个参数,每个参数都有一个类型和一个名称。参数用逗号分隔开。每个参数都在函数的代码中用作一个变量。 例如,下面是一个简单的函数,带有两个参数,并返回它们的乘积: static double product(double param1, double param2) { return param1 * param2; } 下面看一个比较复杂的示例。 试试看:通过函数交换数据(1) (1) 在目录C:\BegVCSharp\Chapter6下创建一个新控制台应用程序Ch06Ex02。 (2) 把下述代码添加到Program.cs中: class Program { static int MaxValue(int[] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal; }
static void Main(string[] args) { int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; int maxVal = MaxValue(myArray); Console.WriteLine("The maximum value in myArray is {0}", maxVal); Console.ReadKey(); } } (3) 执行代码,结果如图6-2所示。
图 6-2 示例的说明 这段代码包含一个函数,它执行的任务就是本章引言中示例函数所完成的任务。该函数的参数是一个整数数组,返回该数组中的最大值。该函数的定义如下所示: static int MaxValue(int[] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal; } 函数MaxValue()定义了一个参数,即int数组intArray,它还有一个int类型的返回值。最大值的计算是很简单的。局部整型变量maxVal初始化为数组中的第一个值,然后把这个值与数组中后面的每个元素依次进行比较。如果一个元素的值比maxVal大,就用这个值代替maxVal的当前值。循环结束时,maxVal就包含数组中的最大值,用return语句返回。 Main()中的代码声明并初始化一个简单的整数数组,用于MaxValue()函数: int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; 调用MaxValue(),把一个值赋给int变量maxVal: int maxVal = MaxValue(myArray); 接着,使用Console.WriteLine()把这个值写到屏幕上: Console.WriteLine("The maximum value in myArray is {0}", maxVal); 1. 参数匹配 在调用函数时,必须使参数与函数定义中指定的参数完全匹配,这意味着要匹配参数的类型、个数和顺序。例如,下面的函数: static void myFunction(string myString, double myDouble) { ... } 不能使用下面的代码调用: myFunction (2.6, "Hello"); 这里试图把一个double值作为第一个参数传递,把string值作为第二个参数传递,参数的顺序与函数声明中定义的顺序不匹配。 也不能使用下面的代码: myFunction("Hello"); 这里仅传送了一个string参数,而该函数需要两个参数。 使用上述两个函数调用都会产生编译错误,因为编译器要求必须匹配函数的签名。 再回过头来看看这个示例,MaxValue()只能用于获取整数数组中的最大值int。如果用下面的代码替换Main()中的代码: static void Main(string[] args) { double[] myArray = {1.3, 8.9, 3.3, 6.5, 2.7, 5.3}; double maxVal = MaxValue(myArray); Console.WriteLine("The maximum value in myArray is {0}", maxVal); Console.ReadKey(); } 这段代码就不能编译,因为参数类型是错误的。 在本章后面的“重载函数”一节将介绍解决这个问题的一个有效技术。 2. 参数数组 C#允许为函数指定一个(只能指定一个)特定的参数,这个参数必须是函数定义中的最后一个参数,称为参数数组。参数数组可以使用个数不定的参数调用函数,它可以使用params关键字来定义。 参数数组可以简化代码,因为不必从调用代码中传递数组,而是传递可在函数中使用的一个数组中相同类型的几个参数。 定义使用参数数组的函数时,需要使用下述代码: static ( , ... , params [] ) { ... return ; } 使用下面的代码可以调用该函数。 (, ... , , , ...) 其中, 等都是类型为的值,用于初始化数组。在可以指定的参数个数方面没有限制。甚至可以根本不指定参数。惟一的限制是它们都必须是类型。 这一点使参数数组特别适合于为在处理过程中要使用的函数指定其他信息。例如,假定有一个函数GetWord(),它的第一个参数是一个string值,并返回字符串中的第一个单词。 string firstWord = GetWord("This is a sentence."); 其中firstWord被赋予字符串This。 可以在GetWord()中添加一个params参数,以根据其下标选择另一个要返回的单词: string firstWord = GetWord("This is a sentence.", 2); 假定第一个单词计数为1,则firstWord就被赋予字符串is。 也可以在第3个参数中限制返回的字符个数,同样通过params参数来实现: string firstWord = GetWord("This is a sentence.", 4, 3); 其中firstWord被赋予字符串sen。 下面看一个完整的示例。这个示例定义并使用带有params类型参数的函数。 试试看:通过函数交换数据(2) (1) 在目录C:\BegVCSharp\Chapter6下创建一个控制台应用程序Ch06Ex03。 (2) 把下述代码添加到Program.cs中: class Program { static int SumVals(params int[] vals) { int sum = 0; foreach (int val in vals) { sum += val; } return sum; }
static void Main(string[] args) { int sum = SumVals(1, 5, 2, 9, 8); Console.WriteLine("Summed Values = {0}", sum); Console.Readkey(); } } (3) 执行代码,结果如图6-3所示。
图 6-3 示例的说明 在这个示例中,函数sumVals()是用关键字params定义的,可以接受任意个int参数(或不接受任何参数): static int SumVals(params int[] vals) { ... } 这个函数对vals数组中的值进行迭代,把这些值加在一起,返回其结果。 在Main()中,用5个整型参数调用这个函数: int sum = SumVals (1, 5, 2, 9, 8); 也可以用0、1、2或100个整型参数调用这个函数—— 参数的个数没有限制。 3. 引用参数和值参数 到目前为止,本章定义的所有函数都带有值参数。其含义是,在使用参数时,是把一个值传递给函数使用的一个变量。对函数中此变量的任何修改都不影响函数调用中指定的参数。例如,下面的函数使传递过来的参数值加倍,并显示出来: static void showDouble(int val) { val *= 2; Console.WriteLine("val doubled = {0}", val); } 参数val在这个函数中被加倍,如果以下面的方式调用它: int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); showDouble(myNumber); Console.WriteLine("myNumber = {0}", myNumber); 输出到控制台上的文本如下所示: myNumber = 5 val doubled = 10 myNumber = 5 把myNumber作为一个参数,调用showDouble()并不影响Main()中myNumber的值,即使分配给val的参数被加倍,myNumber的值也不变。 这很不错,但如果要改变myNumber的值,就会有问题。可以使用一个给myNumber返回新值的函数: static void DoubleNum(int val) { val *= 2; return val; } 并使用下面的代码调用它: int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); myNumber = DoubleNum(myNumber); Console.WriteLine("myNumber = {0}", myNumber); 但这段代码一点也不直观,且不能改变用作参数的多个变量值(因为函数只有一个返回值)。 此时可以通过引用传递参数。即函数处理的变量与函数调用中使用的变量相同,而不仅仅是值相同的变量。因此,对这个变量进行的任何改变都会影响用作参数的变量值。为此,只需使用ref关键字指定参数: static void showDouble(ref int val) { val *= 2; Console.WriteLine("val doubled = {0}", val); } 在函数调用中(这是必须的,因为ref参数是函数签名的一部分): int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); showDouble(ref myNumber); Console.WriteLine("myNumber = {0}", myNumber); 输出到控制台上的文本如下所示: myNumber = 5 val doubled = 10 myNumber = 10 这次,myNumberhas被showDouble()修改了。 用作ref参数的变量有两个限制。首先,函数可能会改变引用参数的值,所以必须在函数调用中使用变量。所以,下面的代码是非法的: const int myNumber = 5; Console.WriteLine("myNumber = {0}", myNumber); showDouble(ref myNumber); Console.WriteLine("myNumber = {0}", myNumber); 其次,必须使用初始化过的变量。C#不允许假定ref参数在使用它的函数中初始化,下面的代码也是非法的: int myNumber; showDouble(ref myNumber); Console.WriteLine("myNumber = {0}", myNumber); 4. 输出参数 除了根据引用传递值之外,还可以使用out关键字,指定所给的参数是一个输出参数。out关键字的使用方式与ref关键字相同(在函数定义和函数调用中用作参数的修饰符)。实际上,它的执行方式与引用参数完全一样,因为在函数执行完毕后,该参数的值将返回给函数调用中使用的变量。但是,这里有一些重要区别。 ● 把未赋值的变量用作ref参数是非法的,但可以把未赋值的变量用作out参数。 ● 另外,在函数使用out参数时,该参数必须看作是还未赋值。即调用代码可以把已赋值的变量用作out参数,存储在该变量中的值会在函数执行时丢失。 例如,考虑前面返回数组中最大值的MaxValue()函数,略微修改该函数,获取数组中最大值的元素下标,为了简单起见,如果数组中有多个元素的值都是这个最大值,只提取第一个最大值的下标。为此,修改函数,添加一个输出参数,如下所示: static int MaxValue(int[] intArray, out int maxIndex) { int maxVal = intArray[0]; maxIndex = 0; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) { maxVal = intArray[i]; maxIndex = i; } } return maxVal; } 可以用下述方式使用该函数: int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; int maxIndex; Console.WriteLine("The maximum value in myArray is {0}", MaxValue(myArray, out maxIndex)); Console.WriteLine("The first occurrence of this value is at element {0}", maxIndex + 1); 结果是: The maximum value in myArray is 9 The first occurrence of this value is at element 7 注意,必须在函数调用中使用out关键字,就像ref关键字一样。 注意: 在屏幕上显示结果时,给返回的maxIndex的值加上1。这样可以使下标更容易读懂,因此数组的第一个元素指元素1,而不是元素0。 6.2 变量的作用域 在上一节中,读者可能想知道为什么需要利用函数交换数据。原因是C#中的变量仅能从代码的本地作用域访问。给定的变量有一个作用域,访问该变量要通过这个作用域来实现。 变量的作用域是一个重要的主题,最好用一个示例来说明。下面的示例将演示变量在一个作用域中定义,但试图在另一个作用域中使用的情形。 试试看:定义和使用基本函数 (1) 对Ch06Ex01中的Program.cs进行如下修改: class Program { static void Write() { Console.WriteLine("myString = {0}", myString); }
static void Main(string[] args) { string myString = "String defined in Main()"; Write(); Console.ReadKey(); } } (2) 编译代码,注意显示在任务列表中的错误和警告: The name 'myString' does not exist in the current context The variable 'myString' is assigned but its value is never used 示例的说明 什么地方出错了?在应用程序主体(Main()函数)中定义的变量myString不能在Write()函数中访问。 原因是变量有一个作用域,在这个作用域中,变量才是有效的。这个作用域包括定义变量的代码块和直接嵌套在其中的代码块。函数中的代码块与调用它们的代码块是不同的。在Write()中,没有定义myString,在Main()中定义的myString则超出了作用域—— 它只能在Main()中使用。 实际上,在Write()中可以有一个完全独立的变量myString,修改代码,如下所示: class Program { static void Write() { string myString = "String defined in Write()"; Console.WriteLine("Now in Write()"); Console.WriteLine("myString = {0}", myString); }
static void Main(string[] args) { string myString = "String defined in Main()"; Write(); Console.WriteLine("\nNow in Main()"); Console.WriteLine("myString = {0}", myString); Console.ReadKey(); } } 这段代码就可以编译,结果如图6-4所示。
图 6-4 这段代码执行的操作如下: ● Main()定义和初始化字符串变量 myString。 ● Main() 把控制权传送给Write()。 ● Write()定义和初始化一个字符串变量myString,它与Main()中定义的myString变量完全不同。 ● Write()把一个字符串输出到控制台上,该字符串包含在Write()中定义的myString的值。 ● Write()把控制权传送回Main()。 ● Main()把一个字符串输出到控制台上,该字符串包含在Main()中定义的myString的值。 作用域以这种方式覆盖一个函数的变量称为局部变量。还有一种全局变量,其作用域可覆盖几个函数。修改代码,如下所示: class Program { static string myString;
static void Write() { string myString = "String defined in Write()"; Console.WriteLine("Now in Write()"); Console.WriteLine("Local myString = {0}", myString); Console.WriteLine("Global myString = {0}", Program.myString); }
static void Main(string[] args) { string myString = "String defined in Main()"; Program.myString = "Global string"; Write(); Console.WriteLine("\nNow in Main()"); Console.WriteLine("Local myString = {0}", myString); Console.WriteLine("Global myString = {0}", Program.myString); Console.ReadKey(); } } 结果如图6-5所示。
图 6-5 这里添加了另一个变量myString,这次进一步加深了代码中的名称层次。这个变量定义如下: static string myString; 注意这里也需要static关键字。在这种形式的控制台应用程序中,必须使用static 或 const关键字,来定义这种形式的全局变量。如果要修改全局变量的值,就需要使用static,因为const禁止修改变量的值。 为了区分这个变量和Main()与Write()中同名的局部变量,必须用一个完整限定的名称为变量名分类,参见第3章。这里把全局变量称为Program.myString。注意,在全局变量和局部变量同名时,这是必需的。如果没有局部myString变量,就可以使用myString表示全局变量,而不需要使用Program.myString。如果局部变量和全局变量同名,全局变量就会被屏蔽。 全局变量的值在Main()中设置如下: Program.myString = "Global string"; 在Write()中访问: Console.WriteLine("Global myString = {0}", Program.myString); 为什么不能使用这个技术通过函数交换数据,而要使用前面介绍的参数来交换数据?有时,这确实是一种交换数据的首选方式,但在许多情况下不应使用这种方式。是否使用全局变量取决于函数的位置。使用全局变量的问题在于,它们一般不适合于“常规用途”的函数—— 这些函数能处理我们所提供的数据,而不仅限于处理特定全局变量中的数据。详见本章后面的内容。 6.2.1 其他结构中变量的作用域 在继续之前,应先注意一下上一节的一个要点总结了上述内容,并超出了函数之间的变量作用域。前面说过,变量的作用域包含定义它们的代码块和直接嵌套在其中的代码块。这也可以应用到其他代码块上,例如分支和循环结构的代码块。考虑下面的代码:
int i; for (i = 0; i < 10; i++) { string text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); } Console.WriteLine("Last text output in loop: {0}", text); 字符串变量text是for循环的局部变量,这段代码不能编译,因为在该循环外部调用的Console.WriteLine()试图使用该变量text,这超出了循环的作用域。修改代码,如下所示: int i; string text; for (i = 0; i < 10; i++) { text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); } Console.WriteLine("Last text output in loop: {0}", text); 这段代码也会失败,原因是变量必须在使用前声明和初始化,而text是在for循环中初始化的。赋给text的值在循环块退出时就丢失了。但是还可以进行如下修改: int i; string text = ""; for (i = 0; i < 10; i++) { text = "Line " + Convert.ToString(i); Console.WriteLine("{0}", text); } Console.WriteLine("Last text output in loop: {0}", text); 这次text是在循环外部初始化的,可以访问它的值。这段简单代码的结果如图6-6所示。
图 6-6 在循环中最后赋给text的值可以在循环外部访问。 可以看出,这个主题的内容需要花一点时间来掌握。在前面的示例中,循环之前赋给text空字符串,而在循环之后的代码中,该text就不会是空字符串了,其原因不能立即看出。 这种情况的解释涉及到分配给text变量的内存空间,实际上任何变量都是这样。只声明一个简单的变量类型,并不会引起其他的变化。只有在给变量赋值后,这个值才占用一块内存空间。如果这种占据内存空间的行为在循环中发生,该值实际上定义为一个局部值,在循环的外部会超出了其作用域。 即使变量本身没有局部化到循环上,循环所包含的值也局部化到该循环上。但是,在循环外部赋值可以确保该值是主体代码的局部值,在循环内部它仍处于其作用域中。这意味着变量在退出主体代码块之前是没有超出作用域的,所以可以在循环外部访问它的值。 幸而,C#编译器可检测变量作用域的问题,它生成的响应错误信息可以帮助我们理解变量作用域的问题。 最后一个要注意的问题是,应采用“最佳实践”。一般情况下,最好在声明和初始化所有的变量后,再在代码块中使用它们。一个例外是把循环变量声明为循环块的一部分,例如: for (int i = 0; i < 10; i++) { ... } 其中i局部化于循环代码块中,但这是可以的,因为我们很少需要在外部代码中访问这个计数器。 6.2.2 参数和返回值与全局数据 本节详细介绍如何通过全局数据以及参数和返回值,与函数交换数据。先看看下面的代码: class Program { static void showDouble(ref int val) { val *= 2; Console.WriteLine("val doubled = {0}", val); }
static void Main(string[] args) { int val = 5; Console.WriteLine("val = {0}", val); showDouble(ref val); Console.WriteLine("val = {0}", val); } } 注意: 这段代码与本章前面的代码略有不同,在前面的示例中,在Main()中使用了变量名myNumber,这说明了局部变量可以有相同的名称,且不会相互干涉。这里列出的两个代码示例比较类似,以便我们集中精力研究它们的区别,而无需担心变量名。 和下面的代码比较: class Program { static int val;
static void showDouble() { val *= 2; Console.WriteLine("val doubled = {0}", val); }
static void Main(string[] args) { val = 5; Console.WriteLine("val = {0}", val); showDouble(); Console.WriteLine("val = {0}", val); } } 这两个showDouble()函数的结果是相同的。 现在,使用哪种方法并没有什么硬性规定,这两种方法都是有效的。但是,需要考虑一些规则。 首先,在第一次讨论这个问题时,使用全局值的showDouble()版本只使用全局变量val。为了使用这个版本,必须使用这个全局变量。这会对该函数的多样性有轻微的限制,如果要存储结果,就必须总是把这个全局变量值复制到其他变量中。另外,全局数据可以在应用程序的其他地方由代码修改,这会导致预料不到的结果(即使我们没有认识到这一点,值也是可以改变的)。 但是,损失了多样性常常是有好处的。我们常常希望把一个函数只用于一个目的,使用全局数据存储能减少在函数调用中犯错的可能性,例如把它传递给错误的变量。 当然,也可以说,这种简化实际上使代码更难理解。显示指定参数可以一眼看出发生了什么改变。例如myFunction(val1, out val2)函数调用,其中val1和val2都是要考虑的重要变量,在函数执行结束后,val2就会被赋予一个新值。反之,如果这个函数不带参数,就不能对它处理了什么数据做任何假设。 最后,记住并不总是能使用全局数据。本书的后面将介绍在不同的文件中编写的代码,以及不同命名空间中的代码如何通过函数彼此通信。像这样的情况,代码常常要分开编写,显然不能使用全局存储方式。 总之,可以自由选择使用哪种技术来交换数据。一般情况下,最好使用参数,而不使用全局数据,但有时使用全局数据更合适,使用这个技术并没有错。 6.3 Main()函数 前面介绍了创建和使用函数时涉及的大多数简单技术,下面详细论述Main()函数。 Main()是C#应用程序的入口点,执行这个函数就是执行应用程序。也就是说,在执行过程开始时,会执行Main()函数,在Main()函数执行完毕时,执行过程就结束了。这个函数有一个参数string[] args,但我们还没有说明这个参数的含义。本节将介绍该参数,以及如何使用它。
注意: Main函数可以使用4种签名: ● static void Main() ● static void Main(string[] args) ● static int Main() ● static int Main(string[] args) 如果需要,可以忽略这里讨论的args。直到现在还在使用这个参数的原因,就是在VS中创建控制台应用程序时自动生成的Main()版本。 上面的第三、四个版本返回一个int值,它们可以用于表示应用程序如何终止,通常用作一种错误提示(但这不是强制的),一般情况下,返回0反映了“正常”的终止(即应用程序执行完毕,并安全地终止)。 Main()的参数args是从应用程序的外部接受信息的方法,这些信息在运行期间指定,其形式是命令行参数。 前面已经遇到了命令行参数,在从命令行上执行应用程序时,通常可以直接指定信息,如在执行应用程序时加载一个文件。例如,考虑Windows中的Notepad应用程序。在命令行窗口中输入notepad,或者在Windows的Start菜单中选择Run选项,再在打开的窗口中输入notepad,就可以运行该应用程序。也可以输入notepad "myfile.txt",结果是Notepad在运行时将加载文件myfile.txt,如果该文件不存在,Notepad也会创建该文件。这里myfile.txt是一个命令行参数。利用args参数,可以编写以相同的方式工作的控制台应用程序。 在执行控制台应用程序时,指定的任何命令行参数都放在这个args数组中,接着可以根据需要在应用程序中使用这些参数。 下面用一个示例来说明。这个示例可以指定任意数量的命令行参数,每个参数都输出到控制台上。 试试看:命令行参数 (1) 在目录C:\BegVCSharp\Chapter6下创建一个新控制台应用程序Ch06Ex04。 (2) 把下述代码添加到Program.cs中: class Program { static void Main(string[] args) { Console.WriteLine("{0} command line arguments were specified:", args.Length); foreach (string arg in args) Console.WriteLine(arg); Console.ReadKey(); } } (3) 打开项目的属性页面(在Solution Explorer窗口中右击Ch06Ex04项目名称,选择Properties)。
(4) 选择Debug页面,在Command Line Arguments设置中添加所希望的命令行参数,如图6-7所示。
图 6-7 (5) 运行应用程序,结果如图6-8所示。
图 6-8 示例的说明 这里使用的代码非常简单: Console.WriteLine("{0} command line arguments were specified:", args.Length); foreach (string arg in args) Console.WriteLine(arg); 使用args参数与使用其他字符串数组类似。我们没有对参数进行任何异样的操作,只是把指定的信息写到屏幕上。 在本示例中,通过VS中的项目属性提供参数,这是一种很便捷的方式,只要在VS中运行应用程序,就可以使用相同的命令行参数,无需每次都在命令行提示窗口中输入它们。在项目输出所在的目录(C:\BegVCSharp\Chapter6\Ch06Ex04\bin\Debug)下打开命令行窗口,输入下述代码,也可以得到相同的结果: Ch06Ex04 256 myFile.txt "a longer argument" 注意,每个参数都用空格分隔开,如果参数包含空格,就可以用双引号把参数括起来,这样才不会把这个参数解释为多个参数。 6.4 结构函数 第5章介绍了结构类型,它可在一个地方存储多个数据元素,结构可以做的工作远不止此。一个重要的功能就是结构可以包含函数和数据。这初看起来很奇怪,但实际上是非常有用的。 例如,考虑下面的结构: struct customerName { public string firstName, lastName; } 如果变量的类型是customerName,并且要在控制台上输出一个完整的名称,就必须从其组件部分建立该名称。例如,CustomerName变量customer可以使用下述语法: customerName myCustomer; myCustomer.firstName = "John"; myCustomer.lastName = "Franklin"; Console.WriteLine("{0} {1}", mycustomer.firstName, mycustomer.lastName); 把函数添加到结构中,就可以集中处理常见任务,简化这个过程。可以把合适的函数添加到结构类型中,如下所示: struct customerName { public string firstName, lastName;
public string Name () { return firstName + " " + lastName; } } 看起来这与本章前面的其他函数很类似,但没有使用static修饰符。原因本书在后面论及,现在知道该关键字不是结构函数所必须的即可。这个函数的用法如下所示: customerName myCustomer; myCustomer.firstName = "John"; myCustomer.lastName = "Franklin"; Console.WriteLine(mycustomer.Name()); 这个语法比前面的语法简单得多,也更容易理解。 注意,Name()函数可以直接访问firstName 和 lastName结构成员,在customerName结构中,它们可以看作是全局成员。 6.5 函数的重载 本章的前面介绍了在调用函数时,必须匹配函数的签名。这表明,需要让多个函数操作不同类型的变量。函数重载允许创建同名的多个函数,这些函数可使用不同的参数类型。 例如,前面使用了下述代码,其中包含一个函数MaxValue(): class Program { static int MaxValue(int[] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal; }
static void Main(string[] args) { int[] myArray = {1, 8, 3, 6, 2, 5, 9, 3, 0, 2}; int maxVal = MaxValue(myArray); Console.WriteLine("The maximum value in myArray is {0}", maxVal); Console.ReadKey(); } } 这个函数只能用于处理int数组,现在要为不同的参数类型提供不同名称的函数,可以把上述函数重命名为IntArrayMaxValue(),添加函数DoubleArrayMaxValue()处理其他类型。另外,还可以在代码中添加如下函数: ... static double MaxValue(double[] doubleArray) { double maxVal = doubleArray[0]; for (int i = 1; i < doubleArray.Length; i++) { if (doubleArray[i] > maxVal) maxVal = doubleArray[i]; } return maxVal; } ... 这里的区别是使用了double值。函数名称MaxValue()是相同的,但其签名是不同的。用相同的名称和签名定义两个函数是错误的,但因为这两个函数有不同的签名,所以是可行的。 现在有两个版本的MaxValue(),它们的参数是int 和 double数组,分别返回一个int或double最大值。 这种代码的优点是不必显示指定要使用哪个函数。只需提供一个数组参数,就可以根据使用的参数类型执行相应的函数。 此时,应注意VS中IntelliSense的另一个功能。如果在应用程序中有上述两个函数,而且要在Main()中输入函数的名称,VS就可以显示出可用的重载函数。如果输入下面的代码: double result = MaxValue( VS提供了MaxValue()两个版本的信息,使用上下箭头键可以在它们之间滚动,如图6-9所示。
图 6-9 在重载函数时,应包括函数签名的所有方面。例如有两个不同的函数,它们分别带有值参数和引用参数: static void showDouble(ref int val) { ... }
static void showDouble(int val) { ... } 选择使用哪个版本纯粹是根据函数调用是否包含ref关键字来确定。下面的代码是调用引用版本: showDouble(ref val); 下面的代码是调用值版本: showDouble(val); 另外,函数还可以根据参数的个数等来区分。 6.6 委托 委托是一种可以把引用存储为函数的类型。这听起来相当棘手,但其机制是非常简单的。委托最重要的用途在本书介绍到事件和事件处理时才能解释清楚,但这里也将介绍有关委托的许多内容。在本书的后面使用它们时,这些内容有助于理解一些比较复杂的问题。 委托的声明非常类似于函数,但不带函数体,且要使用delegate关键字。委托的声明指定了一个函数签名,其中包含一个返回类型和参数列表。在定义了委托后,就可以声明该委托类型的变量。接着把这个变量初始化为与委托有相同签名的函数引用。之后,就可以使用委托变量调用这个函数,就像该变量是一个函数一样。 有了引用函数的变量后,还可以执行不能用其他方式完成的操作。例如,可以把委托变量作为参数传递给一个函数,这样,该函数就可以使用委托调用它引用的任何函数,而且在运行之前无需知道调用的是哪个函数。 下面的示例使用委托访问两个函数中的一个。 试试看:使用委托来调用函数 (1) 在目录C:\BegVCSharp\Chapter6下创建一个新控制台应用程序Ch06Ex05。 (2) 把下述代码添加到Program.cs中: class Program { delegate double ProcessDelegate(double param1, double param2); static double Multiply(double param1, double param2) { return param1 * param2; } static double Divide(double param1, double param2) { return param1 / param2; }
static void Main(string[] args) { ProcessDelegate process; Console.WriteLine("Enter 2 numbers separated with a comma:"); string input = Console.ReadLine(); int commaPos = input.IndexOf(','); double param1 = Convert.ToDouble(input.Substring(0, commaPos)); double param2 = Convert.ToDouble(input.Substring(commaPos + 1, input.Length - commaPos - 1)); Console.WriteLine("Enter M to multiply or D to divide:"); input = Console.ReadLine(); if (input == "M") process = new processDelegate(Multiply); else process = new processDelegate(Divide); Console.WriteLine("Result: {0}", process(param1, param2)); Console.ReadKey(); } } (3) 执行代码,结果如图6-10所示。
图 6-10 示例的说明 这段代码定义了一个委托ProcessDelgate,其签名与两个函数Multiply()和Divide()的签名相匹配。委托的定义如下所示: delegate double ProcessDelegate(double param1, double param2); delegate关键字指定该定义是用于委托的,而不是用于函数的(该定义所在的位置与函数定义相同)。接着,用一个签名指定double返回类型和两个double参数。实际使用的名称可以是任意的,所以可以给委托类型和参数指定任意名称。这里委托名是ProcessDelgate,double参数名是param1 和 param2。 Main()中的代码首先使用新的委托类型声明一个变量: static void Main(string[] args) { ProcessDelegate process; 接着用一些比较标准的C#代码请求由逗号分开的两个数字,并把这些数字放在两个double变量中: Console.WriteLine("Enter 2 numbers separated with a comma:"); string input = Console.ReadLine(); int commaPos = input.IndexOf(','); double param1 = Convert.ToDouble(input.Substring(0, commaPos)); double param2 = Convert.ToDouble(input.Substring(commaPos + 1, input.Length - commaPos - 1)); 注意: 为了说明问题,这里没有包含用户输入的有效性验证。如果这些是“现实中的”代码,就应花更多的时间来确保在局部变量param1 和 param2中得到有效的值。 接着,询问用户是要相乘,还是相除这两个数字: Console.WriteLine("Enter M to multiply or D to divide:"); input = Console.ReadLine(); 根据用户的选择,初始化process委托变量: if (input == "M") process = new processDelegate(Multiply); else process = new processDelegate(Divide); 要把一个函数引用赋给委托变量,需要使用略显古怪的语法。这个过程比较类似于给数组赋值,必须使用new关键字创建一个新委托。在这个关键字的后面,指定委托类型,提供一个引用函数的参数,该函数是Multiply()或Divide()。注意这个参数与委托类型或目标函数的参数不匹配,这是委托赋值的一个独特语法,参数是要使用的函数名,且不带括号。 最后,使用该委托调用所选的函数。无论委托引用的是什么函数,该语法都是有效的: Console.WriteLine("Result: {0}", process(param1, param2)); Console.ReadKey(); } 这里把委托变量看作一个函数名。但与函数不同,我们还可以对这个变量执行更多的操作,例如通过参数把它传递给一个函数,这个函数的一个简单示例如下: static void executeFunction(ProcessDelegate process) { process(2.2, 3.3); } 就像选择一个要使用的“插件”一样,把它们传递给函数委托,就可以控制函数的执行。例如,一个函数要对字符串数组按照字母进行排序。对列表排序有几个不同的方法,它们的性能取决于要排序的列表特性。使用委托可以把一个排序算法函数委托传递给排序函数,指定要使用的方法。 委托有许多用途,但如上所述,它们的大多数常见用途主要与事件处理有关,详见第13章。 6.7 小结 本章相当全面地介绍了C#代码中函数的使用。函数提供的许多其他特性(特别是委托)比较抽象,我们将在面向对象编程中讨论它们,面向对象编程将在第8章讨论。 本章的主要内容总结如下: ● 在控制台应用程序中定义和使用函数 ● 通过返回值和参数,与函数交换数据 ● 给函数传送参数数组 ● 按引用或按值传递参数 ● 为其他返回值指定参数 ● 变量作用域的概念,变量在不需要它们的代码块中可以隐藏起来 ● Main()函数的细节,包括命令行参数的用法 ● 在结构类型中使用函数 ● 函数的重载,可以为同一个函数提供不同的参数,获得其他功能 ● 委托以及如何在运行期间动态地选择函数 如何使用函数的知识是将来要完成的所有编程工作的中心。后面的章节,特别是学习OOP(从第8章开始)的部分,将介绍函数的形式结构,以及如何把它们应用于类。从现在开始,把代码放在可重用块中将成为C#编程中最有用的部分。 6.8 练习 (1) 下面两个函数都有错误,请指出这些错误。 static bool Write() { Console.WriteLine("Text output from function."); }
static void myFunction(string label, params int[] args, bool showLabel) { if (showLabel) Console.WriteLine(label); foreach (int i in args) Console.WriteLine("{0}", i); } (2) 编写一个应用程序,该程序使用两个命令行参数,分别把值放在一个字符串和一个整型变量中,然后显示这些值。 (3) 创建一个委托,在请求用户输入时,使用它模拟Console.ReadLine()函数。 (4) 修改下面的结构,使之包含一个返回订单总价格的函数。 struct order { public string itemName; public int unitCount; public double unitCost; } (5) 在order结构中添加另一个函数,该结构返回一个格式化的字符串,以合适的值替换用尖括号括起来的斜体条目。 Order Information: - items at $ each, total cost $
|
| 商品简介: |
|
本书适合于想学习使用.NET Framework编写C#程序的初级读者,也适合于已了解.NET 1.0,而想学习.NET 2.0和Visual Studio 2005最新功能的读者。
|
| 相关商品: |
 没有收藏任何商品 | |
| 网友评论:(评论内容只代表网友观点,与本站立场无关!) |
【发表评论】 |
|