C#编程介绍
本篇文章为个人的一些关于C#基本语法的总结梳理,希望能够有所帮助:
- 了解C#和.net相关概念;
- 了解C#的基本语法以及相关特性;
- 熟悉C#的编程的风格和结构;
需要声明的是这篇文章并没有包含最新的C#版本的语法糖或其他优化,但对新手学习了解C#是足够的。
一、基本概念
1、C#简介
C#是微软公司在2000年6月发布的一种由C和C++衍生出来的面向对象的编程语言、运行于.NET Framework和.NET Core(完全开源,跨平台)之上的高级程序设计语言。作者是安德斯·海尔斯伯格(Anders Hejlsberg),也是.net的作者。C#是一种安全的、稳定的、简单的、优雅的面向对象编程语言。它在继承C和C++强大功能的同时去掉了一些它们的复杂特性(例如没有宏以及不允许多重继承)。C#综合了VB(Visual Basic)简单的可视化操作和C++的高运行效率,以其强大的操作能力、优雅的语法风格、创新的语言特性和便捷的面向组件编程的支持成为.NET开发的首选语言。此种语言的实现,提供对于以下软件工程要素的支持:强类型检查、数组维度检查、未初始化的变量引用检测、自动垃圾收集。
2、.Net简介
.Net其实就是一个平台,一个供开发者开发的技术平台,在这个平台上,开发者需要遵循平台的语言规范,使用平台提供的工具,然后创造出各种应用程序,从PC端到手机app,再到web,几乎全部涉及。
.Net生态系统
从下往上看, 可以分为三大部分
- 底层支持平台,包括编译器,运行时及CSharp/FSharp/VB.Net等
- 标准类库,统一大部分通用的API
- 应用模型, 适用于不同的操作系统
.NET 5.0(2020年11月正式版发布):
.NET 5.0是.NET Framework和.NET Core核心的结合,旨在统一.NET平台,微软将其描述为**“.NET的未来”**。
.Net体系结构
**官方简介如下:**C# 程序在 .NET 上运行,而 .NET 是名为公共语言运行时 (CLR) 的虚执行系统和一组统一的类库。CLR 是 Microsoft 对公共语言基础结构 (CLI) 国际标准的商业实现。 CLI 是创建执行和开发环境的基础,语言和库可以在其中无缝地协同工作。
用 C# 编写的源代码被编译成符合 CLI 规范的中间语言 (IL)。 IL 代码和资源(如位图和字符串)存储在程序集中,扩展名通常为 .dll。 程序集包含一个介绍程序集的类型、版本和区域性的清单。
执行 C# 程序时,程序集将加载到 CLR。 CLR 会直接执行实时 (JIT) 编译,将 IL 代码转换成本机指令。 CLR 可提供其他与自动垃圾回收、异常处理和资源管理相关的服务。 CLR 执行的代码有时称为“托管代码”(而不是“非托管代码”),被编译成面向特定系统的本机语言。
语言互操作性是 .NET 的一项重要功能。 C# 编译器生成的 IL 代码符合公共类型规范 (CTS)。 通过 C# 生成的 IL 代码可以与通过 .NET 版本的 F#、Visual Basic、C++ 或其他 20 多种与 CTS 兼容的任何语言所生成的代码进行交互。 一个程序集可能包含多个用不同 .NET 语言编写的模块,且类型可以相互引用,就像是用同一种语言编写的一样。
除了运行时服务之外,.NET 还包含大量库。 这些库支持多种不同的工作负载。 它们已整理到命名空间中,这些命名空间提供各种实用功能,包括文件输入输出、字符串控制、XML 分析、Web 应用程序框架和 Windows 窗体控件。 典型的 C# 应用程序广泛使用 .NET 类库来处理常见的“管道”零碎工作。
CLI(common language infrastructure)
通用语言基础架构是微软联合惠普等巨头与2000年向ECMA提交的技术规范,该规范是开放的。它规定了如何在运行库中声明、使用、管理类型,同时也是运行库支持跨语言互操作的一个重要组成部分。
CLI要达到的目的:
- 建立一个支持跨语言集成,类型安全和高性能代码执行的框架
- 提供一个支持完整实现多种编程语言的面向对象的模型。
- 定义各种语言必须遵守的规则,有助于确保用不能语言编写的对象能够交互作用。
CTS(common type system)
对于各种强类型编程语言,定义变量时必须指定变量类型。每一种编程语言所包含的变量类型不尽相同,即使是同一个数据类型。表达方式也各不相同。.Net在设计中博采众长,借鉴了各种主流编程语言的长处,对各种不同的编程语言的数据类型进行了抽象就有了CTS。
CLS(Common language specification)
CTS中定义了大量的类型,这些类型是CLR都能识别的类型。但是并没有要求每种语言都要完全实现。因此在CTS这个大的集合中,截取了其中一部分,要求各种语言都支持,否则无法实现互操作。这一小部分称作“公共语言规范”
CLR(Common language runtime)
主要负责管理 .Net应用程序的编译,运行,为应用程序提供内存分配,线程管理,安全以及垃圾回收等服务,以及负责对代码实施严格的类型安全检查,以保证代码安全、正确地运行。
.Net平台与应用
从开发者与机器的角度,NET平台是.NET应用与操作系统之间的一个中介,首先它为.NET应用运行提供了环境,其次它为.NET应用与操作系统之间起到了“解耦”的作用,使得平台上层的应用不依赖于操作系统(的机器指令集)。
粗略地说,一个.NET应用是一个运行于.Net框架(.NET Framework,.NET Core)之上的应用程序。或者,一个.NET应用是一个使用.NET 类库来编写,并运行于公共语言运行时CLR(通用语言运行时)之上的应用程序。
3、C#的优势
C#不仅仅是介绍的那么简单,网上会有人说这门语言用来对抗JAVA的,不能说完全这样,但确实有一定关系。在C#的语法中能看到许多JAVA的影子。因为Anders最开始搞得叫 VJ++,后来被sun(Java的开创公司)告了,还输了。所以就有了这门语言的产生。这门语言一大优势就是它是微软主推的。在.Net平台上有着无法比拟的优势。
C#作为主流语言之一自然有他的独特之处,通过类比分析大概体现在以下几个方面:
- **更先进的语法体系:**作为一门比较后产生的语言,相比于C/C++和JAVA有一些独特的语法规则,为实际编程开发带来便利,例如事件、委托、属性等等。不能说完全先进,但确实有自己独有的语法糖。而且他在不断进步。
- **强大的IDE工具和技术支撑:**Visual Studio等。(微软亲儿子该有的待遇)
同时它的缺点也异常明显,首先C#是一个产品,它有太多的局限性,尽管微软在推动C#和.net的开源,但毕竟晚了很多年,在生态打造上需要时间,但通过.net core,.net5能看出微软是确实在作这个事情的。
4. 从C++认识C#
1、 C#与Java类似,编译后得到的还不是机器代码,而是运行在虚拟机中的元指令。它对安全性做了更多的考虑,不推荐指针,不建议直接操作内存,他有自动实现内存管理。C++中的指针在带来强大的灵活性和高效的同时,也带了不少使用上的难题,C++程序中的绝大多数问题都来源于指针的不正确使用,C#出于软件安全性的考虑和语言易用性的考虑不推荐使用指针。C#中实现自动垃圾回收,通过new在堆中创建对象,当对该对象的引用计数为0时回收内存。类有构造函数而没有析够函数。
不考虑指针的情况下, C#只有引用和数值之分。int等内部数据类型和struct定义的类型是数据类型,拷贝时做深度拷贝;而string和用class定义的类型是引用类型,拷贝时做浅拷贝——与深度拷贝对应,它通过引用计数来实现对象和内存管理。
C++中用指针能够轻易实现的功能,C#需要引进许多额外的机制。比如C++的函数指针,在C#中称之为delegate。C#中的参数传递,分为传值和传址两种,传址时需要加ref或者out(传回改变)关键字。
C#中的const与C++中的有所不同,它指编译期常量,而运行期间的常量要用readonly来指定。
2、C#的面向对象特性更为彻底,一切皆对象,不存在独立的函数,程序的入口Mai()函数是某个对象的public static成员函数。
所有对象都是由Object派生而来,包括内部数据类型int,float,string等,它们只是System.int32等的别名而已。C#中没有模板,通过将参数设置为Object类型来实现类似的功能。
C#没有头文件,变量、函数和类没有定义和申明的区别,都在一起。只能通过设计抽象类的方式实现代码分离。
二、语法规则
1、程序结构
一个简单的例子:
using System;
namespace HelloWorldApplication
{
class HelloWorld
{
static void Main(string[] args)
{
// 打印Hello World
Console.WriteLine("Hello World");
Console.ReadKey();
}
}
}
一个简单的helloworld程序:
-
程序的第一行 using System; - using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句。
-
下一行是 namespace 声明。一个 namespace 里包含了一系列的类。HelloWorldApplication 命名空间包含了类 HelloWorld。
-
下一行是 class 声明。类 HelloWorld 包含了程序使用的数据和方法声明。类一般包含多个方法。方法定义了类的行为。在这里,HelloWorld类只有一个 Main 方法。
-
下一行定义了 Main 方法,是所有 C# 程序的 入口点。Main 方法说明当执行时 类将做什么动作。main方法必须放在类内。
-
下一行 // 将会被编译器忽略,且它会在程序中添加额外的 注释。
-
Main 方法通过语句 Console.WriteLine("Hello World"); 指定了它的行为。
WriteLine 是一个定义在 System 命名空间中的 Console 类的一个方法。该语句会在屏幕上显示消息 "Hello World"。
-
最后一行 Console.ReadKey(); 是针对 VS.NET 用户的。这使得程序会等待一个按键的动作,防止程序从 Visual Studio .NET 启动时屏幕会快速运行并关闭。
1.1 标识符
标识符是用来识别类、变量、函数或任何其它用户定义的项目。在 C# 中,类的命名必须遵循如下基本规则:
- 标识符必须以字母、下划线或 @ 开头,后面可以跟一系列的字母、数字( 0 - 9 )、下划线( _ )、@。
- 标识符中的第一个字符不能是数字。
- 标识符必须不包含任何嵌入的空格或符号,比如 ? - +! # % ^ & * ( ) [ ] { } . ; : " ' / \。
- 标识符不能是 C# 关键字。除非它们有一个 @ 前缀。 例如,@if 是有效的标识符,但 if 不是,因为 if 是关键字。
- 标识符必须区分大小写。大写字母和小写字母被认为是不同的字母。
- 不能与C#的类库名称相同。
1.2 C# 关键字
关键字是 C# 编译器预定义的保留字。这些关键字不能用作标识符,但是,如果您想使用这些关键字作为标识符,可以在关键字前面加上 @ 字符作为前缀。
在 C# 中,有些关键字在代码的上下文中有特殊的意义,如 get 和 set,这些被称为上下文关键字(contextual keywords)。
下表列出了 C# 中的保留关键字(Reserved Keywords)和上下文关键字(Contextual Keywords):
| 保留关键字 | ||||||
|---|---|---|---|---|---|---|
| abstract | as | base | bool | break | byte | case |
| catch | char | checked | class | const | continue | decimal |
| default | delegate | do | double | else | enum | event |
| explicit | extern | false | finally | fixed | float | for |
| foreach | goto | if | implicit | in | in (generic modifier) | int |
| interface | internal | is | lock | long | namespace | new |
| null | object | operator | out | out (generic modifier) | override | params |
| private | protected | public | readonly | ref | return | sbyte |
| sealed | short | sizeof | stackalloc | static | string | struct |
| switch | this | throw | true | try | typeof | uint |
| ulong | unchecked | unsafe | ushort | using | virtual | void |
| volatile | while | |||||
| 上下文关键字 | ||||||
| add | alias | ascending | descending | dynamic | from | get |
| global | group | into | join | let | orderby | partial (type) |
| partial (method) | remove | select | set |
2、基础语法
2.1 数据类型
大部分的类型与C++相似,可以大体分为类:
值类型
| 类型 | 描述 | 范围 | 默认值 |
|---|---|---|---|
| bool | 布尔值 | True 或 False | False |
| byte | 8 位无符号整数 | 0 到 255 | 0 |
| char | 16 位 Unicode 字符 | U +0000 到 U +ffff | '\0' |
| decimal | 128 位精确的十进制值,28-29 有效位数 | (-7.9 x 1028 到 7.9 x 1028) / 100 到 28 | 0.0M |
| double | 64 位双精度浮点型 | (+/-)5.0 x 10-324 到 (+/-)1.7 x 10308 | 0.0D |
| float | 32 位单精度浮点型 | -3.4 x 1038 到 + 3.4 x 1038 | 0.0F |
| int | 32 位有符号整数类型 | -2,147,483,648 到 2,147,483,647 | 0 |
| long | 64 位有符号整数类型 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0L |
| sbyte | 8 位有符号整数类型 | -128 到 127 | 0 |
| short | 16 位有符号整数类型 | -32,768 到 32,767 | 0 |
| uint | 32 位无符号整数类型 | 0 到 4,294,967,295 | 0 |
| ulong | 64 位无符号整数类型 | 0 到 18,446,744,073,709,551,615 | 0 |
| ushort | 16 位无符号整数类型 | 0 到 65,535 | 0 |
char在C#中表示16 位 Unicode 字符,与C++中的wchar_t对应。
decimal定义的变量具有极高的精度,但在使用的时候需要在数据后面加上M或m,否者会默认为double型致使报错。
decimal x = 3.33m;
引用类型
引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用。
换句话说,它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的 引用类型有:object、dynamic 和 string。
- 对象(Object)类型
对象(Object)类型 是 C# 通用类型系统(Common Type System - CTS)中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。
当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱。
int val = 8;
object obj = val;//先装箱
int nval = (int)obj;//再拆箱
- 动态(Dynamic)类型
您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。
声明动态类型的语法:
dynamic <variable_name> = value;
例如:
dynamic d = 20;
动态类型与对象类型相似,但是对象类型变量的类型检查是在编译时发生的,而动态类型变量的类型检查是在运行时发生的。
- 字符串(String)类型
字符串(String)类型 允许给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。
例如:
String str = "runoob.com";
一个 @引号字符串:
@"runoob.com";
C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待,比如:
string str = @"C:\Windows";
等价于:
string str = "C:\\Windows";
@ 字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内。
string str = @"<script type=""text/javascript"">
<!--
-->
</script>";
指针类型
作为一种托管代码,使用指针并不是C#所建议的,但是C#还是留下了一种使用指针的方式,也就是不安全代码:当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块。
using System;
namespace UnsafeCodeApplication
{
class Program
{
static unsafe void Main(string[] args)
{
int var = 20;
int* p = &var;
Console.WriteLine("Data is: {0} ", var);
Console.WriteLine("Address is: {0}", (int)p);
Console.ReadKey();
}
void fun()
{
unsafe
{
// ...
}
// ...
}
}
}
但是使用不安全代码是需要进行编译器调整的,vs下需要更改属性。
可空类型
C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值。例如,Nullable< Int32 >,读作"可空的 Int32",可以被赋值为 -2,147,483,648 到 2,147,483,647 之间的任意值,也可以被赋值为 null 值。类似的,Nullable< bool > 变量可以被赋值为 true 或 false 或 null。
在处理数据库和其他包含可能未赋值的元素的数据类型时,将 null 赋值给数值类型或布尔型的功能特别有用。例如,数据库中的布尔型字段可以存储值 true 或 false,或者,该字段也可以未定义。
? : 单问号用于对 int,double,bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 Nullable 类型的。
int? i = 3;
等同于:
Nullable<int> i = new Nullable<int>(3);
int i; //默认值0
int? ii; //默认值null
Null 合并运算符用于定义可空类型和引用类型的默认值。
?? : 双问号 用于判断一个变量在为 null 时返回一个指定的值。判断??左边的对象是否为 null,如果不为 null 则使用 ?? 左边的对象,如果为 null 则使用 ?? 右边的对象。
比如:
a = b ?? c
如果 b 为 null,则 a = c,如果 b 不为 null,则 a = b。
2.2 类型转换
在 C# 中,类型转换有两种形式:
- 隐式类型转换 - 这些转换是 C# 默认的以安全方式进行的转换, 不会导致数据丢失。例如,从小的整数类型转换为大的整数类型,从派生类转换为基类。
- 显式类型转换 - 显式类型转换,即强制类型转换。显式转换需要强制转换运算符,而且强制转换会造成数据丢失。
Convert类
通过Convert类进行转换,Convert类中提供了很多转换的方法。使用这些方法的前提是能将需要转换的对象转换成相应的类型,如果不能转换则会报格式不对的错误。
Convert类常用的类型转换方法
| 方法 | 说明 |
|---|---|
| Convert.ToInt32() | 转换为整型(int) |
| Convert.ToChar() | 转换为字符型(char) |
| Convert.ToString() | 转换为字符串型(string) |
| Convert.ToDateTime() | 转换为日期型(datetime) |
| Convert.ToDouble() | 转换为双精度浮点型(double) |
| Conert.ToSingle() | 转换为单精度浮点型(float) |
Convert.ToInt32() 与 int.Parse() 的区别
这个两个都是将字符串转换为int型的方式,但是有细微的区别。没搞清楚 Convert.ToInt32 和 int.Parse() 的细细微区别时千万别乱用,否则可能会产生无法预料的结果,举例来说:假如从 url 中取一个参数 page 的值,我们知道这个值是一个 int,所以即可以用 Convert.ToInt32(Request.QueryString["page"]),也可以用 int.Parse(Request.QueryString["page"]),但是如果 page 这个参数在 url 中不存在,那么前者将返回 0,0 可能是一个有效的值,所以你不知道 url 中原来根本就没有这个参数而继续进行下一下的处理,这就可能产生意想不到的效果,而用后一种办法的话没有 page 这个参数会抛出异常,我们可以捕获异常然后再做相应的处理,比如提示用户缺少参数,而不是把参数值当做 0 来处理。
(1) 这两个方法的最大不同是它们对 null 值的处理方法: Convert.ToInt32(null) 会返回 0 而不会产生任何异常,但 int.Parse(null) 则会产生异常。
(2) 对数据进行四舍五入时候的区别
- Convert.ToInt32(double value) 如果 value 为两个整数中间的数字,则返回二者中的偶数;即 3.5 转换为 4,4.5 转换为 4,而 5.5 转换为 6。不过 4.6 可以转换为 5,4.4 转换为 4 。
- int.Parse("4.5") 直接报错:"输入字符串的格式不正确"。
- int(4.6) = 4 Int 转化其他数值类型为 Int 时没有四舍五入,强制转换。
(3) 对被转换类型的区别 int.Parse 是转换 String 为 int, Convert.ToInt32 是转换继承自 Object 的对象为 int 的(可以有很多其它类型的数据)。你得到一个 object 对象, 你想把它转换为 int, 用 int.Parse 就不可以, 要用 Convert.ToInt32。
as
使用AS操作符转换只能用于引用类型和可为空的类型。使用as有很多好处,当无法进行类型转换时,会将对象赋值为NULL,避免类型转换时报错或是出异常。C#抛出异常在进行捕获异常并进行处理是很消耗资源的,如果只是将对象赋值为NULL的话是几乎不消耗资源的(消耗很小的资源)。
当我们直接使用强制类型转换的时候:
Object obj = new NewTpe();
NewType newObj = (NewType)obj;
在这个过程中是不安全的,还要使用try catch来进行保护。
这样一来,代码就变成了:
Object obj1 = new NewType();
NewType newValue = null;
try
{
newValue = (NewType)obj1;
}
catch (Exception err)
{
MessageBox.Show(err.Message);
}
不过在C#中这样的方法比较低效,这里推荐使用关键字as:
Object obj1 = new NewType();
NewType newValue = obj1 as NewType;
- 安全性:
as操作符不会做过多的转换操作,当需要转化对象的类型属于转换目标类型或者转换目标类型的派生类型时,那么此转换操作才能成功,而且并不产生新的对象(当不成功的时候,会返回null)。因此用as进行类型转换是安全的。
- 效率:
当用as操作符进行类型转换的时候,首先判断当前对象的类型,当类型满足要求后才进行转换,而传统的类型转换方式,是用当前对象直接去转换,而且为了保护转换成功,要加上try-catch,所以,相对来说,as效率高点。
需要注意的是,不管是传统的还是as操作符进行类型转换之后,在使用之前,需要进行判断转换是否成功,如下:
if(newValue != null)
{
//Work with the object named “newValue“
}
不过as也要注意以下两点:
1.不能在类型之间进行转化,会出现编译错误(相互有继承关系除外):
NewType newValue = new NewType();
NewTYpe1 newValue1 = newValue as NewTYpe1;//错误
2.不能用在值类型数据,也会出现编译错误:
Object obj1 = 11;
int nValue = obj1 as int;//错误
is
Is:检查对象是否与给定的类型兼容。例如,下面的代码可以确定MyObject类型的一个实例,或者对象是否从MyObject派生的一个类型:
if(obj is MyObject){}
如果所提供的表达式非空,并且所提供的对象可以强制转换为所提供的类型而不会导致引发异常,则 is 表达式的计算结果将是 true。
如果已知表达式始终是true或始终是false,则is关键字将导致编译时警告,但是通常在运行时才计算类型兼容性。
Object myObject = new Object();
Boolean b1 = (myObject is Object); //true.
Boolean b2 = (myObject is Employee); //false.
如果对象引用是null,is运算符总是返回false,因为没有可检查其类型的对象。
is运算符通常像下面这样使用:
if (myObject is Employee)
{
Employee myEmployee = (Employee)myObject;
}
在这段代码中,CLR实际会检查两次对象的类型。is运算符首先核实myObject是否兼容于Employee类型。如果是,那么在if语句内部执行转换型,CLR会再次核实myObject是否引用一个Employee。CLR的类型检查增加了安全性,但这样对性能造成一定影响。这是因为CLR首先必须判断变量(myObject)引用的对象的实际类型。然后,CLR必须遍历继承层次结构,用每个基类型去核对指定的类型(Employee)。由于这是一个相当常用的编程模式,所以C#专门提供了as运算符,目的就是简化这种代码写法,同时提升性能。
我们可以使用as 来提高性能:
Employee myEmployee = myObject as Employee;
if (myEmployee != null)
{ }
在这段代码中,CLR核实myObject是否兼容于Employee类型;如果是,as会返回对同一个对象的一个非null的引用。如果myObject不兼容于Employee类型,as运算符会返回null。
as运算符的工作方式与强制类型转换一样,只是它永远不会抛出一个异常。相反,如果对象不能转换,结果就是null。所以,正确的做法是检查最终生成的一引用是否为null。如果企图直接使用最终生成的引用,会抛出一个System.NullReferenceException异常。
Object o = new Object(); //新建一个Object对象。
Employee e = o as Employee; //将o转型为一个Employee
e.ToString(); //访问e会抛出一个NullReferenceException异常
is也支持Pattern Matching(模式匹配),
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
public class Square
{
public double Side { get; }
public Square(double side)
{
Side = side;
}
}
public class Circle
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
}
public struct Rectangle
{
public double Length { get; }
public double Height { get; }
public Rectangle(double length, double height)
{
Length = length;
Height = height;
}
}
public class Triangle
{
public double Base { get; }
public double Height { get; }
public Triangle(double @base, double height)
{
Base = @base;
Height = height;
}
}
/* it is a classic expression of the type pattern before C#7.0:
* testing a variable to determine its type
* and taking a different action based on that type
*/
public class is_test
{
public static double ComputeArea(object shape)
{
if (shape is Square)
{
var s = (Square)shape;
return s.Side * s.Side;
}
else if (shape is Circle)
{
var c = (Circle)shape;
return c.Radius * c.Radius * Math.PI;
}
else if (shape is Rectangle)
{
var r = (Rectangle)shape;
return r.Height * r.Length;
}
// elided
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: shape.GetType().ToString());
}
/*the is expression both test the variable
*and assigns it to a new variable of the proper type
*/
public static double ComputeAreaModernIs(object shape)
{
if (shape is Square s)
return s.Side * s.Side;
else if (shape is Circle c)
return c.Radius * c.Radius * Math.PI;
else if (shape is Rectangle r)
return r.Height * r.Length;
// elided
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
}
2.3 运算符
C# 中的运算符优先级
| 类别 | 运算符 | 结合性 |
|---|---|---|
| 后缀 | () [] -> . ++ - - | 从左到右 |
| 一元 | + - ! ~ ++ - - (type)* & sizeof | 从右到左 |
| 乘除 | * / % | 从左到右 |
| 加减 | + - | 从左到右 |
| 移位 | << >> | 从左到右 |
| 关系 | < <= > >= | 从左到右 |
| 相等 | == != | 从左到右 |
| 位与 AND | & | 从左到右 |
| 位异或 XOR | 从左到右 | |
| 位或 OR | | | 从左到右 |
| 逻辑与 AND | && | 从左到右 |
| 逻辑或 OR | || | 从左到右 |
| 条件 | ?: | 从右到左 |
| 赋值 | = += -= *= /= %=>>= <<= &= ^= |= | 从右到左 |
| 逗号 | , | 从左到右 |
2.4 枚举
枚举类型中的每个元素必须通过类型.元素的形式调用
可以++操作
3、数组
在 C# 中声明一个数组,您可以使用下面的语法:
datatype[] arrayName;
其中,
- datatype 用于指定被存储在数组中的元素的类型。
- [ ] 指定数组的秩(维度)。秩指定数组的大小。
- arrayName 指定数组的名称。
例如:
double[] balance;
- 声明数组
在 C# 中声明一个数组,您可以使用下面的语法:
datatype[] arrayName;
其中,
- datatype 用于指定被存储在数组中的元素的类型。
- [ ] 指定数组的秩(维度)。秩指定数组的大小。
- arrayName 指定数组的名称。
例如:
double[] balance;
- 初始化数组
声明一个数组不会在内存中初始化数组。当初始化数组变量时,您可以赋值给数组。
数组是一个引用类型,所以您需要使用 new 关键字来创建数组的实例。
例如:
double[] balance = new double[10];
- 赋值给数组
您可以通过使用索引号赋值给一个单独的数组元素,比如:
double[] balance = new double[10];
balance[0] = 4500.0;
您可以在声明数组的同时给数组赋值,比如:
double[] balance = { 2340.0, 4523.69, 3421.0};
您也可以创建并初始化一个数组,比如:
int [] marks = new int[5] { 99, 98, 92, 97, 95};
在上述情况下,你也可以省略数组的大小,比如:
int [] marks = new int[] { 99, 98, 92, 97, 95};
您也可以赋值一个数组变量到另一个目标数组变量中。在这种情况下,目标和源会指向相同的内存位置:
int [] marks = new int[] { 99, 98, 92, 97, 95};
int[] score = marks;
当您创建一个数组时,C# 编译器会根据数组类型隐式初始化每个数组元素为一个默认值。例如,int 数组的所有元素都会被初始化为 0。
- 访问数组元素
元素是通过带索引的数组名称来访问的。这是通过把元素的索引放置在数组名称后的方括号中来实现的。例如:
double salary = balance[9];
二维数组
多维数组最简单的形式是二维数组。一个二维数组,在本质上,是一个一维数组的列表。一个二维数组可以被认为是一个带有 x 行和 y 列的表格。C# 中二维数组的概念不同于 C/C++、java 等语言中的二维数组,C# 中的二维数组更像是一个矩阵。
- 初始化二维数组
多维数组可以通过在括号内为每行指定值来进行初始化。下面是一个带有 3 行 4 列的数组。
int [,] a = new int [3,4] {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
- 访问二维数组元素
二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的。例如:
int val = a[2,3];
交错数组
交错数组是数组的数组。
交错数组是一维数组。
您可以声明一个带有 int 值的交错数组 scores,如下所示:
int [][] scores;
声明一个数组不会在内存中创建数组。创建上面的数组:
int[][] scores = new int[5][];
for (int i = 0; i < scores.Length; i++)
{
scores[i] = new int[4];
}
您可以初始化一个交错数组,如下所示:
int[][] scores = new int[2][]{new int[]{92,93,94},new int[]{85,66,87,88}};
其中,scores 是一个由两个整型数组组成的数组 -- scores[0] 是一个带有 3 个整数的数组,scores[1] 是一个带有 4 个整数的数组。
交错数组与二维数组的区别,可以直观的理解为交错数组每一行的长度是可以不一样的。
如果说二维数组像是唐诗,那么交错数组就是宋词,每一句的长短没有限制。
// 声明一个交错数组 a,a 中有三个元素,分别是 a[0],a[1],a[2] 每个元素都是一个数组
int[][] a = new int[3][];
//以下是声明交错数组的每一个元素的,每个数组的长度可以不同
a[0] = new int[] { 1,2,3 };
a[1] = new int[] { 4,5,6,7,8 };
a[2] = new int[] { 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 };
传递数组给函数
传递交错数组(或者二维数组)作为参数:
using System;
namespace ArrayApplication
{
class Program
{
static void Main(String[] args)
{
int[][] array = new int[3][];
array[0] = new int[] { 0, 1 };
array[1] = new int[] { 2, 3, 4 };
array[2] = new int[] { 5, 6, 7, 8 };
Program pro = new Program();
int summ=pro.getArea(array); //函数名作为参数
Console.WriteLine("{0}", summ);
}
public int getArea(int[][] array)
{
int sum=0;
for (int j = 0; j < array.Length;j++ )
foreach (int i in array[j])
sum += i;
return sum;
}
}
}
参数数组
在使用数组作为形参时,C# 提供了 params 关键字,使调用数组为形参的方法时,既可以传递数组实参,也可以传递一组数组元素。params 的使用格式为:
public 返回类型 方法名称( params 类型名称[] 数组名称 )
- 1.带 params 关键字的参数类型必须是一维数组,不能使用在多维数组上;
- 2.不允许和 ref、out 同时使用;
- 3.带 params 关键字的参数必须是最后一个参数,并且在方法声明中只允许一个 params 关键字。
- 4.不能仅使用 params 来使用重载方法。
- 5.没有 params 关键字的方法的优先级高于带有params关键字的方法的优先级
using System;
namespace MyParams
{
class MyParams
{
static void Test(int a, int b)
{
Console.WriteLine("a + b = {0}", a + b);
}
static void Test(params int[] list)
{
foreach(int i in list)
{
Console.Write("{0} ", i);
}
}
static void Main(string[] args)
{
Test(1, 2);
Test(1, 2, 3, 4, 5);
Console.ReadLine();
}
}
}
Array 类
C#数组也是一个类,对于数组里面的元素,.net提供了一些列的操作。Array 类是 C# 中所有数组的基类,它是在 System 命名空间中定义。Array 类提供了各种用于数组的属性和方法。下表列出了 Array 类中一些最常用的属性:
| 序号 | 属性 & 描述 |
|---|---|
| 1 | IsFixedSize 获取一个值,该值指示数组是否带有固定大小。 |
| 2 | IsReadOnly 获取一个值,该值指示数组是否只读。 |
| 3 | Length 获取一个 32 位整数,该值表示所有维度的数组中的元素总数。 |
| 4 | LongLength 获取一个 64 位整数,该值表示所有维度的数组中的元素总数。 |
| 5 | Rank 获取数组的秩(维度)。 |
如需了解 Array 类的完整的属性列表,请参阅微软的 C# 文档。
下表列出了 Array 类中一些最常用的方法:
| 序号 | 方法 & 描述 |
|---|---|
| 1 | Clear 根据元素的类型,设置数组中某个范围的元素为零、为 false 或者为 null。 |
| 2 | Copy(Array, Array, Int32) 从数组的第一个元素开始复制某个范围的元素到另一个数组的第一个元素位置。长度由一个 32 位整数指定。 |
| 3 | CopyTo(Array, Int32) 从当前的一维数组中复制所有的元素到一个指定的一维数组的指定索引位置。索引由一个 32 位整数指定。 |
| 4 | GetLength 获取一个 32 位整数,该值表示指定维度的数组中的元素总数。 |
| 5 | GetLongLength 获取一个 64 位整数,该值表示指定维度的数组中的元素总数。 |
| 6 | GetLowerBound 获取数组中指定维度的下界。 |
| 7 | GetType 获取当前实例的类型。从对象(Object)继承。 |
| 8 | GetUpperBound 获取数组中指定维度的上界。 |
| 9 | GetValue(Int32) 获取一维数组中指定位置的值。索引由一个 32 位整数指定。 |
| 10 | IndexOf(Array, Object) 搜索指定的对象,返回整个一维数组中第一次出现的索引。 |
| 11 | Reverse(Array) 逆转整个一维数组中元素的顺序。 |
| 12 | SetValue(Object, Int32) 给一维数组中指定位置的元素设置值。索引由一个 32 位整数指定。 |
| 13 | Sort(Array) 使用数组的每个元素的 IComparable 实现来排序整个一维数组中的元素。 |
| 14 | ToString 返回一个表示当前对象的字符串。从对象(Object)继承。 |
如需了解 Array 类的完整的方法列表,请参阅微软的 C# 文档。
下面的程序演示了 Array 类的一些方法的用法:
using System;
namespace ArrayApplication
{
class MyArray
{
static void Main(string[] args)
{
int[] list = { 34, 72, 13, 44, 25, 30, 10 };
Console.Write("原始数组: ");
foreach (int i in list)
{
Console.Write(i + " ");
}
Console.WriteLine();
// 逆转数组
Array.Reverse(list);
Console.Write("逆转数组: ");
foreach (int i in list)
{
Console.Write(i + " ");
}
Console.WriteLine();
// 排序数组
Array.Sort(list);
Console.Write("排序数组: ");
foreach (int i in list)
{
Console.Write(i + " ");
}
Console.WriteLine();
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
原始数组: 34 72 13 44 25 30 10
逆转数组: 10 30 25 44 13 72 34
排序数组: 10 13 25 30 34 44 72
4、判断
if使用与C++没差别。但是switch...case里面,C#参数类型可以是任何类型,C++则必须是整形或者能默认转换为整型的。
- case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量。
- 当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
- 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
- 不是每一个 case 都需要包含 break。如果 case 语句为空,则可以不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。
- C# 不允许从一个开关部分继续执行到下一个开关部分。如果 case 语句中有处理语句,则必须包含 break 或其他跳转语句。
- 一个 switch 语句可以有一个可选的 default case,出现在 switch 的结尾。default case 可用于在上面所有 case 都不为真时执行一个任务。default case 中的 break 语句不是必需的。
- C# 不支持从一个 case 标签显式贯穿到另一个 case 标签。如果要使 C# 支持从一个 case 标签显式贯穿到另一个 case 标签,可以使用 goto 一个 switch-case 或 goto default
char grade = 'B';
switch (grade)
{
case 'A':
Console.WriteLine("很棒!");
break;
case 'B':
case 'C':
Console.WriteLine("做得好");
break;
case 'D':
Console.WriteLine("您通过了");
break;
case 'F':
Console.WriteLine("最好再试一下");
break;
default:
Console.WriteLine("无效的成绩");
break;
}
Console.WriteLine("您的成绩是 {0}", grade);
同时C#还支持更加强大的Pattern Matching(模式匹配用法)。
Pattern Matching(模式匹配用法)
switch的模式匹配
传统的c风格的switch,对参数有严格的限制,仅允许常量,示例如下:
/*
*it only support the constant pattern
*/
public static string GenerateMessage(params string[] parts)
{
switch (parts.Length)
{
case 0:
return "No elements to the input";
case 1:
return $"One element: {parts[0]}";
case 2:
return $"Two elements: {parts[0]}, {parts[1]}";
default:
return $"Many elements. Too many to write";
}
}
而新的switch的模式匹配已经删除这些限制,代码如下:
public static double ComputeAreaModernSwitch(object shape)
{
switch (shape)
{
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
case Rectangle r:
return r.Height * r.Length;
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
when语句可以在表达式中作为判断条件,代码如下:
public static double ComputeArea_Version3(object shape)
{
switch (shape)
{
case Square s when s.Side == 0:
case Circle c when c.Radius == 0:
case Triangle t when t.Base == 0 || t.Height == 0:
case Rectangle r when r.Length == 0 || r.Height == 0:
return 0;
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
case Triangle t:
return t.Base * t.Height / 2;
case Rectangle r:
return r.Length * r.Height;
case null:
throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
C#9.0又加入更加丰富的语法糖:
首先看传统的例子:
static decimal GetTicket(string sex, int age, string area)
{
if (sex == "男")
{
if (age < 20 && area == "安徽")
{
return 2000;
}
else
{
if (age < 40 && area == "上海")
{
return 4000;
}
else
{
return 3000;
}
}
}
else
{
if (age < 20 && area == "安徽")
{
return 2500;
}
if (age < 60 && area == "安徽")
{
return 1500;
}
}
return 0;
}
Pattern Matching方法:
static decimal GetTicket_Pattern(string sex, int age, string area)
{
return (sex, age, area) switch
{
("男", < 20, "安徽") => 2000,
("男", < 40, "上海") => 4000,
("男", _, _) => 3000,
("女", < 20, "安徽") => 2500,
("女", < 60, "安徽") => 1500,
_ => 0
};
弃元
弃元相当于未赋值的变量;它们没有值。 因为只有一个弃元变量,甚至不为该变量分配存储空间,所以弃元可减少内存分配。 因为它们使代码的意图清楚,增强了其可读性和可维护性。
通过将下划线 (_) 赋给一个变量作为其变量名,指示该变量为一个占位符变量。
5、循环
foreach
C# 也支持 foreach 循环,使用foreach可以迭代数组或者一个集合对象。
以下实例有三个部分:
- 通过 foreach 循环输出整型数组中的元素。
- 通过 for 循环输出整型数组中的元素。
class ForEachTest
{
static void Main(string[] args)
{
int[] fibarray = new int[] { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int element in fibarray)
{
System.Console.WriteLine(element);
}
System.Console.WriteLine();
// 类似 foreach 循环
for (int i = 0; i < fibarray.Length; i++)
{
System.Console.WriteLine(fibarray[i]);
}
}
}
6、字符串
1.string是一个引用类型,平时我们比较string对象,比较的是对象的值而不是对象本身
如:
string strA="abcde";
string strB="abc";
string strC="de";
Console.WriteLine(strA == (strB+strC));//true
Console.WriteLine((object)strA == (object)(strB+strC));//false
因为字符串内容相同但引用的不是同一个实例
2.string对象是不可修改的
string strA="abcde";
strA="aaaaa";
从表面上看似修改了strA的内容,事实上"abcde"没有被修改,而是从新创建了一个对象"aaaaa",然后把该对象的引用赋给strA,最后"abcde"会被作为垃圾回收。
3.string的创建
直接赋值:string strA="abcde";//创建一个内容为abcde的string对象,然后将该对象的引用赋给strA
构造: char[] arr={'a','b','c','d','e'};
string strA=new string(arr);//这里只列举一种
注意:没有String str=new String("abcde");这样的构造,string 是 .NET Framework 中String的别名。
3.string参数传递
string是引用类型,我们试图在一个函数里改变这个值
static void Main(string[] args)
{
string strA = "abcde";
Deal(strA);
Console.WriteLine(strA);
Console.ReadLine();
}
static void Deal(string str)
{
str = str.Substring(0, 2);// 检索字符串,截取
}
结果:abcde
原因:通过值传递引用类型的参数时,有可能更改引用所指向的数据,如某类成员的值。但无法更改引用本身的值,通过ref关键字传递参数可解决该问题。
static void Main(string[] args)
{
string strA = "abcde";
Deal(ref strA);
Console.WriteLine(strA);
Console.ReadLine();
}
static void Deal(ref string str)
{
str = str.Substring(0, 2);
}
结果:ab
此时传递的是引用本身,而不是副本
4.null 字符串和空字符串
null 字符串:没有分配内存;空字符串分配了内存,但内存里面没有数据.
static void Main(string[] args)
{
string strA = "1";
string strB = string.Empty;
string strC = null;
Console.WriteLine(int.Parse(strA));//正确
Console.WriteLine(int.Parse(strB));//输入字符串的格式不正确
Console.WriteLine(strC.ToString());//未将对象引用设置到对象的实例。
Console.ReadLine();
}
内置方法字符串是否为 null 或为空: IsNullOrEmpty等同于 if (str == null || str.Equals(String.Empty)) IsNullOrWhiteSpace等同于 if (str == null || str.Equals(String.Empty) || str.Trim().Equals(String.Empty))
5.StringBuilder
string strA="abc"
for(int i=0;i<10000;i++)
{
strA+="abc";
}
Consolse.WriteLine(strA);
尽管该代码会出现以使用字符串串联来将新的字符追加到命名为 strA 的现有字符串,它实际上会为每个串联操作创建新的 String 对象。大大的降低了性能。可使用 StringBuilder 类代替String 类多次更改字符串值,StringBuilder 对象是可变的,当进行追加或删除字符串中的子字符串时,不会创建新的对象,而是在原来的对象上进行修改。 完成 StringBuilder 对象的值的修改后,可以调用其 StringBuilder.ToString 方法将其转换为字符串
StringBuilder strA=new StringBuilder();
for(int i=0;i<10000;i++)
{
strA.Append("abc");
}
Consolse.WriteLine(strA.ToString());
6.字符串的$符号
在C#中可以利用符号的作用相当于对String.format()的简化。例子如下:
string name = "Horace";
int age = 34;
Console.WriteLine($"He asked, \"Is your name {name}?\", but didn't wait for a reply :-{{");
Console.WriteLine($"{name} is {age} year{(age == 1 ? "" : "s")} old.");
// Expected output is:
// He asked, "Is your name Horace?", but didn't wait for a reply :-{
// Horace is 34 years old.
C# 中 字符串常量可以以 @ 开头声名,这样的优点是转义序列“不”被处理,按“原样”输出,即我们不需要对转义字符加上 \ (反斜扛)也可以原样输出。如,
string filePath = @"c:\Docs\Source\a.txt"
// rather than "c:\\Docs\\Source\\a.txt"
7、方法
C#方法就是对应着C++的函数。在了解方法之前需要先了解C#的访问修饰符。
访问修饰符
C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。
一个 访问修饰符 定义了一个类成员的范围和可见性。C# 支持的访问修饰符如下所示:
- public:所有对象都可以访问;
- private:对象本身在对象内部可以访问;
- protected:只有该类对象及其子类对象可以访问
- internal:同一个程序集的对象可以访问;
- protected internal:访问限于当前程序集或派生自包含类的类型。
形象一点的说明:
比如说:一个人A为父类,他的儿子B(同一程序集的派生类),妻子C(同一程序集的不同类),私生子D(其他程序集的派生类)(注:D不在他家里)
如果我们给A的事情增加修饰符:
- public事件,地球人都知道,全公开
- protected事件,A,B,D知道(A和他的所有儿子知道,妻子C不知道)
- private事件,只有A知道
- internal事件,A,B,C知道(A家里人都知道,私生子D不知道)
- protected internal事件,A,B,C,D都知道,其它人不知道
参数传递
方法中参数的类型有三种
(1)in型参数
int 型参数通过值传递的方式将数值传入方法中。
(2)ref型参数
该种类型的参数传递变量地址给方法(引用传递),传递前变量必须初始化。
该类型与out型的区别在与:
- 1).ref 型传递变量前,变量必须初始化,否则编译器会报错, 而 out 型则不需要初始化
- 2).ref 型传递变量,数值可以传入方法中,而 out 型无法将数据传入方法中。换而言之,ref 型有进有出,out 型只出不进。
(3)out 型参数
与 ref 型类似,仅用于传回结果。
注意:
1). out型数据在方法中必须要赋值,否则编译器会报错。
eg:如下代码若将代码中的sum1方法的方法体
改为 a+=b; 则编译器会报错。原因:out 型只出不进,在没给 a 赋值前是不能使用的
改为 b+=b+2; 编译器也会报错。原因:out 型数据在方法中必须要赋值。
2). 重载方法时若两个方法的区别仅限于一个参数类型为ref 另一个方法中为out,编译器会报错
eg:若将下面的代码中将方法名 sum1 改为 sum(或者将方法名 sum 改为 sum1),编译器会报错。
Error 1 Cannot define overloaded method ‘sum’ because it differs from another method only on ref and out
原因:参数类型区别仅限于 为 ref 与为 out 时,若重载对编译器而言两者的元数据表示完全相同。
class C
{
//1. in型参数
public void sum(int a, int b) {
a += b;
}
//2. ref型参数
public void sum(ref int a, int b)
{
a += b;
}
//3. out型参数
public void sum1(out int a, int b)
{
a = b+2;
}
public static void Main(string[] args)
{
C c = new C();
int a = 1, b = 2;
c.sum(a,b);
Console.WriteLine("a:{0}", a);
a = 1; b = 2;
c.sum(ref a, b);
Console.WriteLine("ref a:{0}", a);
a = 1; b = 2;
c.sum1(out a, b);
Console.WriteLine("out a:{0}", a);
}
}
输出结果:
a:1
ref a:3
out a:4
从代码也可以看出,int 型参数为值传递,所以当将变量 a 传入方法时,变量 a 的值并不会发生变化。而 ref 型参数,由于是引用传递,将变量的值和地址都传入方法中故变量值改变。out 型无法将变量的值传入。但可以将变量的地址传入并为该地址上的变量赋值。
ref 和 out 的区别
牵扯到数据是引用类型还是值类型。
一般用这两个关键字你是想调用一个函数将某个值类型的数据通过一个函数后进行更改。传 out 定义的参数进去的时候这个参数在函数内部必须初始化。否则是不能进行编译的。ref 和 out 都是传递数据的地址,正因为传了地址,才能对源数据进行修改。
一般情况下不加 ref 或者 out 的时候,传值类型的数据进去实际上传进去的是源数据的一个副本,也就是在内存中新开辟了一块空间,这里面存的值是与源数据相等的,这也就是为什么在传值类型数据的时候你如果不用 return 是无法修改原值的原因。但是你如果用了 ref,或者 out,这一切问题都解决了,因为他们传的是地址。
out 比起 ref 来说,还有一个用法就是可以作为多返回值来用,都知道函数只能有一个返回值,C#里,如果你想让一个函数有多个返回值,那么OUT能很容易解决。
多返回值的另一种方式:tuple方法
tuple是一个元组,最多支持7个元素,再多需要嵌套等方法实现。
使用元组定义函数的方法如下:
public static Tuple<string,string> TupleFun()
{
string[] T = {'hello','world'};
Tuple<string, string> tup = new Tuple<string, string>(T[0], T[2]);
return tup;
}
元组还支持多种类型的值。
public static Tuple<string,int> TupleFun()
{
string T = ‘hello’;
int q = 6;
Tuple<string, int> tup = new Tuple<string, int>(T, q);
return tup;
}
还有一种写法:
public (string Name, int Age, bool Male) GetPerson()
{
return ("Jack", 56, true);
}
保证类型对其即可。
扩展方法
扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。对于用 C# 和 Visual Basic 编写的客户端代码,调用扩展方法与调用在类型中实际定义的方法之间没有明显的差异。
扩展方法被定义为静态方法,但它们是通过实例方法语法进行调用的。它们的第一个参数指定该方法作用于哪个类型,并且该参数以 this 修饰符为前缀。仅当你使用 using 指令将命名空间显式导入到源代码中之后,扩展方法才位于范围中。
下面的示例演示为 System.String 类定义的一个扩展方法。
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
可使用此 using 指令将 WordCount 扩展方法置于范围中:
using ExtensionMethods;
而且,可以使用以下语法从应用程序中调用该扩展方法:
string s = "Hello Extension Methods";
int i = s.WordCount();
在代码中,可以使用实例方法语法调用该扩展方法。 但是,编译器生成的中间语言 (IL) 会将代码转换为对静态方法的调用。 因此,并未真正违反封装原则。 实际上,扩展方法无法访问它们所扩展的类型中的私有变量。
由于扩展方法是使用实例方法语法调用的,因此不需要任何特殊知识即可从客户端代码中使用它们。 若要为特定类型启用扩展方法,只需为在其中定义这些方法的命名空间添加 using 指令。
8、结构体
C# 结构体的特点
在 C# 中的结构与传统的 C 或 C++ 中的结构不同。C# 中的结构有以下特点:
- 结构可带有方法、字段、索引、属性、运算符方法和事件。
- 结构可定义构造函数,但不能定义析构函数。但是,您不能为结构定义无参构造函数。无参构造函数(默认)是自动定义的,且不能被改变。
- 与类不同,结构不能继承其他的结构或类。
- 结构不能作为其他结构或类的基础结构。
- 结构可实现一个或多个接口。
- 结构成员不能指定为 abstract、virtual 或 protected。
- 当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。
- 如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。
9、类
当你定义一个类时,你定义了一个数据类型的蓝图。这实际上并没有定义任何的数据,但它定义了类的名称意味着什么,也就是说,类的对象由什么组成及在这个对象上可执行什么操作。对象是类的实例。构成类的方法和变量称为类的成员。
**写入上区别:C#类中所有的方法,字段都有自己的访问修饰符,不接受public:**这种写法。默认均为private。
C# 类的静态成员:
我们可以使用 static 关键字把类成员定义为静态的。当我们声明一个类成员为静态时,意味着无论有多少个类的对象被创建,只会有一个该静态成员的副本。
关键字 static 意味着类中只有一个该成员的实例。静态变量用于定义常量,因为它们的值可以通过直接调用类而不需要创建类的实例来获取。静态变量可在成员函数或类的定义外部进行初始化。你也可以在类的定义内部初始化静态变量。
将类成员函数声明为public static无需实例化即可调用类成员函数。
C++/C#:类Class与结构体Struct的区别
C++中:
默认的访问控制、继承访问权限不同:struct时public的,class时 private的;
其它基本一样。
C#中:
struct是值类型,class是引用类型的;
struct StructA
{
int id ;
}
class ClassA
{
int id ;
}
StructA A ; // 在栈上新建了一个A的实例
ClassA A ; // 声明了一个A的引用,没有新建任何实例(此时A = null,相当于C++中 ClassA* A ;)
ClassA A = new ClassA() ; // 在堆上新建一个A的实例,并在栈上声明一个指向它的引用
注1:C#函数1等价于C++函数1(而非C++函数2)、C#函数2等价于C++函数2
C# 函数1 :void SetId_666(ClassA a) { a.id = 666 ; }
C++函数1:void SetId_666(ClassA* a) { a->id = 666 ; }
C# 函数2 :void SetId_666(StructA a) { a.id = 666 ; }
C++函数2:void SetId_666(ClassA a) { a.id = 666 ; }
注2:C#中与C++中类class行为更接近的是结构体struct,而非类class。
10、继承
虚方法
在子类中用 override 重写父类中用 virtual 申明的虚方法时,实例化父类调用该方法,执行时调用的是子类中重写的方法;
如果子类中用 new 覆盖父类中用 virtual 申明的虚方法时,实例化父类调用该方法,执行时调用的是父类中的虚方法;
/// <summary>
/// 父类
/// </summary>
public class ParentClass
{
public virtual void ParVirMethod()
{
Console.WriteLine("父类的方法...");
}
}
/// <summary>
/// 子类1
/// </summary>
public class ChildClass1 : ParentClass
{
public override void ParVirMethod()
{
Console.WriteLine("子类1的方法...");
}
}
/// <summary>
/// 子类2
/// </summary>
public class ChildClass2 : ParentClass
{
public new void ParVirMethod()
{
Console.WriteLine("子类2的方法...");
}
public void Test()
{
Console.WriteLine("子类2的其他方法...");
}
}
执行调用:
ParentClass par = new ChildClass1();
par.ParVirMethod(); //结果:"子类1的方法",调用子类的方法,实现了多态
par = new ChildClass2();
par.ParVirMethod(); //结果:"父类的方法",调用父类的方法,没有实现多态
深究其原因,为何两者不同,是因为原理不同: override是重写,即将基类的方法在派生类里直接抹去重新写,故而调用的方法就是子类方法;而new只是将基类的方法在派生类里隐藏起来,故而调用的仍旧是基类方法。
多重继承
多重继承指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。
C# 不支持多重继承。但是,您可以使用接口来实现多重继承。
接口
接口定义了所有类继承接口时应遵循的语法合同。接口定义了语法合同 "是什么" 部分,派生类定义了语法合同 "怎么做" 部分。
接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。
接口使得实现接口的类或结构在形式上保持一致。
抽象类在某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。
简单的使用:
using System;
interface IMyInterface
{
// 接口成员
void MethodToImplement();
}
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
}
多重继承实例:
using System;
using System.Collections.Generic;
using System.Text;
//通过接口实现多重继承
namespace interfaceDemo
{
class Person {//定义实体类
internal int age;
internal string name;
bool isMale;
public Person(int age, string name, bool isMale) {//构造方法
this.age = age;
this.name = name;
this.isMale = isMale;
}
public bool IsMale() {
return this.isMale;
}
}
interface Teacher//定义接口
{
string GetSchool();//接口里的方法都是抽象的
string GetSubject();
}
interface Doctor
{
string GetHospital();
double GetSalary();
}
class PersonA : Person, Teacher//继承person类、实现Teacher接口
{
public PersonA(int age, string name, bool isMale)
: base(age, name, isMale)//派生类调用积累的构造方法
{
}
public string GetSchool()//实现接口的抽象方法
{
return "清华大学";
}
public string GetSubject()//实现接口的抽象方法
{
return "经济学";
}
}
class PersonB : Person, Teacher, Doctor//继承person类,同时实现两个接口
{
public PersonB(int age, string name, bool isMale)
: base(age, name, isMale)
{
}
public string GetSchool()
{
return "北京大学";
}
public string GetSubject()//实现接口的抽象方法
{
return "计算机";
}
public string GetHospital() {
return "北京附属医院";
}
public double GetSalary() {
return 2000;
}
}
class TestInterface//测试类
{
static void Main(string[] args) {
PersonA p1 = new PersonA(40, "张三", true);
string gender = "";
if (p1.IsMale())
{
gender = "男";
}
else {
gender = "女";
}
Console.WriteLine("{0},{1},{2}岁,{3}教师,专业是{4}", p1.name, gender, p1.age,
p1.GetSchool(), p1.GetSubject());//调用接口已经实现的方法
PersonB p2 = new PersonB(55, "赵六", false);
if (p2.IsMale())
{
gender = "男";
}
else
{
gender = "女";
}
Console.WriteLine("{0},{1},{2}岁,{3}教师,专业是{4},\n同时也是{5}医生,工资为{6}",
p2.name, gender, p2.age, p2.GetSchool(), p2.GetSubject(),p2.GetHospital(),p2.GetSalary());
Console.ReadLine();
}
}
}
11、多态性
多态是同一个行为具有多个不同表现形式或形态的能力。
多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。
多态性可以是静态的或动态的。在静态多态性中,函数的响应是在编译时发生的。在动态多态性中,函数的响应是在运行时发生的。
在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object。
静态多态性
在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。C# 提供了两种技术来实现静态多态性。分别为:
- 函数重载
- 运算符重载
运算符重载将在下一章节讨论,接下来我们将讨论函数重载。
函数重载
您可以在同一个范围内对相同的函数名有多个定义。函数的定义必须彼此不同,可以是参数列表中的参数类型不同,也可以是参数个数不同。不能重载只有返回类型不同的函数声明。
运算符重载
可以重定义或重载 C# 中内置的运算符。因此,程序员也可以使用用户自定义类型的运算符。重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的。与其他函数一样,重载运算符有返回类型和参数列表。
例子如下:
public static Box operator+ (Box b, Box c)
{
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}
上面的函数为用户自定义的类 Box 实现了加法运算符(+)。它把两个 Box 对象的属性相加,并返回相加后的 Box 对象。
注意:运算符重载必须是public和static修饰的。
下表描述了 C# 中运算符重载的能力:
| 运算符 | 描述 |
|---|---|
| +, -, !, ~, ++, -- | 这些一元运算符只有一个操作数,且可以被重载。 |
| +, -, *, /, % | 这些二元运算符带有两个操作数,且可以被重载。 |
| ==, !=, <, >, <=, >= | 这些比较运算符可以被重载。 |
| &&, || | 这些条件逻辑运算符不能被直接重载。 |
| +=, -=, *=, /=, %= | 这些赋值运算符不能被重载。 |
| =, ., ?:, ->, new, is, sizeof, typeof | 这些运算符不能被重载。 |
12、预处理器
预处理器指令指导编译器在实际编译开始之前对信息进行预处理。预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。
所有的预处理器指令都是以 # 开始。且在一行上,只有空白字符可以出现在预处理器指令之前。预处理器指令不是语句,所以它们不以分号(;)结束。
C# 编译器没有一个单独的预处理器,但是,指令被处理时就像是有一个单独的预处理器一样。在 C# 中,预处理器指令用于在条件编译中起作用。与 C 和 C++ 不同的是,它们不是用来创建宏。一个预处理器指令必须是该行上的唯一指令。
C# 预处理器指令列表
下表列出了 C# 中可用的预处理器指令:
| 预处理器指令 | 描述 |
|---|---|
| #define | 它用于定义一系列成为符号的字符。 |
| #undef | 它用于取消定义符号。 |
| #if | 它用于测试符号是否为真。 |
| #else | 它用于创建复合条件指令,与 #if 一起使用。 |
| #elif | 它用于创建复合条件指令。 |
| #endif | 指定一个条件指令的结束。 |
| #line | 它可以让您修改编译器的行数以及(可选地)输出错误和警告的文件名。 |
| #error | 它允许从代码的指定位置生成一个错误。 |
| #warning | 它允许从代码的指定位置生成一级警告。 |
| #region | 它可以让您在使用 Visual Studio Code Editor 的大纲特性时,指定一个可展开或折叠的代码块。 |
| #endregion | 它标识着 #region 块的结束。 |
测试发现#define必须写在文件的最上面,甚至在using system之前。
13、异常处理
异常是在程序执行期间出现的问题。C# 中的异常是对程序运行时出现的特殊情况的一种响应,比如尝试除以零。
异常提供了一种把程序控制权从某个部分转移到另一个部分的方式。C# 异常处理时建立在四个关键词之上的:try、catch、finally 和 throw。
- try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
- catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
- finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。例如,如果您打开一个文件,不管是否出现异常文件都要被关闭。
- throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。
C# 中的异常类
C# 异常是使用类来表示的。C# 中的异常类主要是直接或间接地派生于 System.Exception 类。System.ApplicationException 和 System.SystemException 类是派生于 System.Exception 类的异常类。
System.ApplicationException 类支持由应用程序生成的异常。所以程序员定义的异常都应派生自该类。
System.SystemException 类是所有预定义的系统异常的基类。
下表列出了一些派生自 Sytem.SystemException 类的预定义的异常类:
| 异常类 | 描述 |
|---|---|
| System.IO.IOException | 处理 I/O 错误。 |
| System.IndexOutOfRangeException | 处理当方法指向超出范围的数组索引时生成的错误。 |
| System.ArrayTypeMismatchException | 处理当数组类型不匹配时生成的错误。 |
| System.NullReferenceException | 处理当依从一个空对象时生成的错误。 |
| System.DivideByZeroException | 处理当除以零时生成的错误。 |
| System.InvalidCastException | 处理在类型转换期间生成的错误。 |
| System.OutOfMemoryException | 处理空闲内存不足生成的错误。 |
| System.StackOverflowException | 处理栈溢出生成的错误。 |
三、进阶语法
1、C# 特性(Attribute)
我们在写代码时通常都要求写注释,为了是让别人或自己以后能看得懂,但是这个注释是写给“人”看的,突发奇想一下:我们能不能写出一种注释,给c#编译器看,比如我们在某些代码上打个标记,让编译器看到这些标记后,做出不同的反应?这就是特性。
**特性(Attribute)**是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。
预定义特性(Attribute)
.Net 框架提供了三种预定义特性:
- AttributeUsage
- Conditional
- Obsolete
AttributeUsage
预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。
规定该特性的语法如下:
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]
其中:
- 参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。
- 参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
- 参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。
Conditional
这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。
它会引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。例如,当调试代码时显示变量的值。
规定该特性的语法如下:
[Conditional(
conditionalSymbol
)]
例如:
[Conditional("DEBUG")]
下面的实例演示了该特性:
#define DEBUG
using System;
using System.Diagnostics;
public class Myclass
{
[Conditional("DEBUG")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
}
class Test
{
static void function1()
{
Myclass.Message("In Function 1.");
function2();
}
static void function2()
{
Myclass.Message("In Function 2.");
}
public static void Main()
{
Myclass.Message("In Main function.");
function1();
Console.ReadKey();
}
}
当上面的代码被编译和执行时,它会产生下列结果:
In Main function
In Function 1
In Function 2
Obsolete
这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。
规定该特性的语法如下:
[Obsolete(
message
)]
[Obsolete(
message,
iserror
)]
其中:
- 参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。
- 参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。
下面的实例演示了该特性:
using System;
public class MyClass
{
[Obsolete("Don't use OldMethod, use NewMethod instead", true)]
static void OldMethod()
{
Console.WriteLine("It is the old method");
}
static void NewMethod()
{
Console.WriteLine("It is the new method");
}
public static void Main()
{
OldMethod();
}
}
当尝试编译该程序时,编译器会给出一个错误消息说明:
Don't use OldMethod, use NewMethod instead
自定义特性
public class MyselfAttribute : System.Attribute
{
private string _name;
private int _age;
private string _memo;
public MyselfAttribute(string name,int age)
{
_name = name;
_age = age;
}
public string Name
{
get { return _name == null ? string.Empty : _name; }
}
public int Age { get { return _age; } }
public string Memo
{
set { _memo = value; }
get { return _memo; }
}
public void ShowName()
{
Console.WriteLine("Hello,{0}", _name == null ? "word." : _name);
}
}
上面定义了一个特性类,单独看它跟普通类没有任何区别,下面看一下如何应用:
[Myself("Emma", 25, Memo = "my good girl.")]
public class Mytest
{
public void SayHello()
{
Console.WriteLine("Hello,my.net world.");
}
}
这里将刚才的MyselfAttribute特性应用到Mytest类上面了,注意写法:后缀Attribute可以省略。
[Myself("Emma", 25, Memo = "my good girl.")]
这一行的含义相当于
new MyselfAttribute("Emma",25){Memo = "my good girl."}
使用方法如下:
01 using System;
02 using System.Reflection;
03 ...
04
05 static void Main(string[] args)
06 {
07 Type info = typeof(Mytest);
08
09 MyselfAttribute myattribute = (MyselfAttribute)Attribute.GetCustomAttribute(info, typeof(MyselfAttribute));
10
11 if (myattribute != null)
12 {
13 Console.WriteLine("Name:{0}", myattribute.Name);
14 Console.WriteLine("Age:{0}", myattribute.Age);
15 Console.WriteLine("Memo of {0} is {1}", myattribute.Name, myattribute.Memo);
16 myattribute.ShowName();
17 }
18
19 //多点反射
20 object obj = Activator.CreateInstance(typeof(Mytest));
21 MethodInfo mi = info.GetMethod("SayHello");
22 mi.Invoke(obj, null);
23 Console.ReadLine();
24
25 }
运行结果:
1 Name:Emma
2 Age:25
3 Memo of Emma is my good girl.
4 Hello,Emma
5 Hello,my.net world.
2、反射
什么是元数据(MetaData)和反射(reflection)
一般情况下我们的程序都在处理数据的读、写、操作和展示。但是有些程序操作的数据不是数字、文本、图片,而是程序和程序类型本身的信息。
-
元数据是包含程序以及类型信息的数据,它保存在程序的程序集当中。
-
程序在运行的时候,可以查看其他程序集或者其本身的元数据。这个行为就是反射。
程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。
可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。
优缺点
优点:
- 1、反射提高了程序的灵活性和扩展性。
- 2、降低耦合性,提高自适应能力。
- 3、它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:
- 1、性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
- 2、使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
反射(Reflection)的用途
反射提供描述程序集、模块和类型的对象。 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问器字段和属性。 如果代码中使用了特性,可以利用反射来访问它们。
下面一个简单的反射示例,使用方法 GetType()(被 Object 基类的所有类型继承)以获取变量类型:
using System;
using System.Reflection;
// Using GetType to obtain type information:
int i = 42;
Type type = i.GetType();
Console.WriteLine(type);
输出为:System.Int32。
反射的典型用法如下所示:
- 使用 Assembly 来定义和加载程序集,加载程序集清单中列出的模块,以及在此程序集中定位一个类型并创建一个它的实例。
- 使用 Module 发现信息,如包含模块的程序集和模块中的类。 还可以获取所有全局方法或模块上定义的其它特定的非全局方法。
- 使用 ConstructorInfo 发现信息,如名称、参数、访问修饰符(如 public 或 private)和构造函数的实现详细信息(如 abstract 或 virtual)。 使用 Type 的 GetConstructors 或 GetConstructor 方法来调用特定构造函数。
- 使用 MethodInfo 发现信息,如名称、返回类型、参数、访问修饰符(如 public 或 private)和方法的实现详细信息(如 abstract 或 virtual)。 使用 Type 的 GetMethods 或 GetMethod 方法来调用特定方法。
- 使用 FieldInfo 发现信息,如名称、访问修饰符(如 public 或 private)和一个字段的实现详细信息 (如 static);并获取或设置字段值。
- 使用 EventInfo 发现信息(如名称、事件处理程序的数据类型、自定义特性、声明类型以及事件的反射的类型),并添加或删除事件处理程序。
- 使用 PropertyInfo 发现信息(如名称、数据类型、声明类型,反射的类型和属性的只读或可写状态),并获取或设置属性值。
- 使用 ParameterInfo 发现信息,如参数的名称、数据类型、参数是输入参数还是输出参数以及参数在方法签名中的位置。
- 使用 CustomAttributeData 在于应用程序域的仅反射上下文中工作时发现有关自定义特性的信息。 CustomAttributeData 使你能够检查特性,而无需创建它们的实例。
3、属性
属性(property)是一种用于访问对象或类的特性的成员。
属性提供灵活的机制来读取、编写或计算私有字段的值。
属性提供了一种机制,它把读取和写入对象的某些特性与一些操作关联起来。
可以像使用公共数据成员一样使用属性,但实际上属性是称为“访问器”的一种特殊方法,这使得数据在被轻松访问的同时,仍能提供方法的安全性和灵活性。
在 C#中,也可以通过字段和一对读写方法,自己手动实现属性:
class A
{
private int c;
public int getC()
{
return c;
}
public void setC(int value)
{
c = value;
}
}
在这里的私有字段称为支持字段(Backing Field)。
不过,这样做有两个明显缺点,一是必须手打这些代码;二是在访问属性时,必须调用方法,而不能直接使用点号加属性名。
CLR 提供了称为属性的机制,解决了这两个缺点。下面的写法是经过简化了的写法:
private int c { get; set; }
如果不想属性有任何特殊行为,从 C# 3 开始,可以使用简易语法get; set;。
这样创建的属性叫做自动实现的属性。另外,我们可以直接通过 A.c 访问属性,而非使用 A.getC 和 A.setC 方法了。可以通过将 get 或 set 设置为 private 获得只读和只写属性。
我们还可以设置只读或只写。例如,如果 set 是私有的,则属性就是只读的。不过,属性的值仍然可以被类型内部的成员修改:
public int c { get; private set; }
从 C# 6 开始,允许省略 set 获得真正具有不变性的属性:
public int c { get; }
索引器
索引器(Indexer) 允许一个对象可以像数组一样使用下标的方式来访问。
当为类定义一个索引器时,该类的行为就会像一个 虚拟数组(virtual array) 一样。可以使用数组访问运算符 [ ] 来访问该类的的成员。
索引器的行为的声明在某种程度上类似于属性(property)。就像属性(property),您可使用 get 和 set 访问器来定义索引器。但是,属性返回或设置一个特定的数据成员,而索引器返回或设置对象实例的一个特定值。换句话说,它把实例数据分为更小的部分,并索引每个部分,获取或设置每个部分。
定义一个属性(property)包括提供属性名称。索引器定义的时候不带有名称,但带有 this 关键字,它指向对象实例。下面的实例演示了这个概念:
using System;
namespace IndexerApplication
{
class IndexedNames
{
private string[] namelist = new string[size];
static public int size = 10;
public IndexedNames()
{
for (int i = 0; i < size; i++)
namelist[i] = "N. A.";
}
public string this[int index]
{
get
{
string tmp;
if( index >= 0 && index <= size-1 )
{
tmp = namelist[index];
}
else
{
tmp = "";
}
return ( tmp );
}
set
{
if( index >= 0 && index <= size-1 )
{
namelist[index] = value;
}
}
}
static void Main(string[] args)
{
IndexedNames names = new IndexedNames();
names[0] = "Zara";
names[1] = "Riz";
names[2] = "Nuha";
names[3] = "Asif";
names[4] = "Davinder";
names[5] = "Sunil";
names[6] = "Rubic";
for ( int i = 0; i < IndexedNames.size; i++ )
{
Console.WriteLine(names[i]);
}
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.
4、委托
在C#中,委托(delegate)是一种引用类型,在其他语言中,与委托最接近的是函数指针,但委托不仅存储对方法入口点的引用,还存储对用于调用方法的对象实例的引用。
简单的讲委托(delegate)是一种类型安全的函数指针,首先,看下面的示例程序,在C++中使用函数指针。
首先,存在两个方法:分别用于求两个数的最大值和最小值。
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
<Y?X:Y;< font>
}
上面两个函数的特点是:函数的返回值类型及参数列表都一样。那么,我们可以使用函数指针来指代这两个函数,并且可以将具体的指代过程交给用户,这样,可以减少用户判断的次数。
下面我们可以建立一个函数指针,将指向任意一个方法,代码如下所示:
//定义一个函数指针,并声明该指针可以指向的函数的返回值为int类型,参数列表中包//括两个int类型的参数
int (*p)(int,int);
//让指针p指向Max函数
p=max;
//利用指针调用Max
c=(*p)(5,6);
我们的问题在于,上面的代码中,为什么不直接使用Max函数,而是利用一个指针指向Max之后,再利用指针调用Max函数呢?
实际上,使用指针的方便之处就在于,当前时刻可以让指针p指向Max,在后面的代码中,我们还可以利用指针p再指向Min函数,但是不论p指向的是谁,调用p时的形式都一样,这样可以很大程度上减少判断语句的使用,使代码的可读性增强!
在C#中,我们可以使用委托(delegate)来实现函数指针的功能,也就是说,我们可以像使用函数指针一样,在运行时利用delegate动态指向具备相同签名的方法(所谓的方法签名,是指一个方法的返回值类型及其参数列表的类型)。
下面的实例演示了委托的声明、实例化和使用,该委托可用于引用带有一个整型参数的方法,并返回一个整型值。
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
// 使用委托对象调用方法
nc1(25);
Console.WriteLine("Value of Num: {0}", getNum());
nc2(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of Num: 35
Value of Num: 175
委托使用的注意事项:
-
在C#中,所有的委托都是从System.Delegate类派生的。
-
委托隐含具有sealed属性,即不能用来派生新的类型。
-
委托最大的作用就是为类的事件绑定事件处理程序。
-
在通过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。
-
委托实例中可以封装静态的方法也可以封装实例方法。
-
在创建委托实例时,需要传递将要映射的方法或其他委托实例以指明委托将要封装的函数原型(.NET中称为方法签名:signature)。注意,如果映射的是静态方法,传递的参数应该是类名.方法名,如果映射的是实例方法,传递的参数应该是实例名.方法名。
-
只有当两个委托实例所映射的方法以及该方法所属的对象都相同时,才认为它们是想等的(从函数地址考虑)。
委托的多播
委托对象可使用 "+" 运算符进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。"-" 运算符可用于从合并的委托中移除组件委托。
使用委托的这个有用的特点,可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的 多播(multicasting),也叫组播。下面的程序演示了委托的多播:
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of Num: 75
匿名方法
在前面的代码中,可以发现,在使用委托时,无论该代码难易,都需要将功能性代码放置在一个方法中,再利用委托指向该方法。但其实C#提供了一种匿名函数的方式。所谓匿名方法(Anonymous Method),是指在使用委托时,可以不再事先定义一个方法,然后再让委托指向方法,匿名委托允许开发人员,使用内联方式,直接让一个委托指向一个功能代码段。下面代码对比了传统方法中委托的会用,以及利用匿名方法的简化操作:
传统方法使用委托:先定义一个方法,再定义委托,并指向方法
public void Run()
{
StreamWriter sw = new StreamWriter("e:\ex\log.txt",true );
for (int i = 0; i < 10000000; i++)
{
sw.WriteLine(i.ToString ());
}
sw.Close();
}
delegate void MyDelegate();
protected void Button2_Click(object sender, EventArgs e)
{
MyDelegate md = new MyDelegate(this.Run );
md();
}
利用匿名方法简化委托的使用
delegate void MyDelegate();
protected void Button2_Click(object sender, EventArgs e)
{
MyDelegate md = new MyDelegate(
delegate()
{
StreamWriter sw = new StreamWriter("e:\ex\log.txt", true);
for (int i = 0; i < 10000000; i++)
{
sw.WriteLine(i.ToString());
}
sw.Close();
}
);
md();
}
从上面代码的对比中,使用匿名方法,省去了定义方法的步骤。
5、事件
类或对象可以通过事件向其他类或对象通知发生的相关事情。
我们可以把事件编程简单地分成两个部分:事件发生的类(书面上叫事件发生器)和事件接收处理的类。事件发生的类就是说在这个类中触发了一个事件,但这个类并不知道哪个个对象或方法将会加收到并处理它触发的事件。所需要的是在发送方和接收方之间存在一个媒介。这个媒介在.NET中就是委托(delegate)。在事件接收处理的类中,我们需要有一个处理事件的方法。在官方论述中将这个过程分为了发布器和订阅器:
发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。
订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。
下面一个实例:
using System;
namespace DelegateAndEvent
{
//定义一个事件类
public class MyEvent
{
//定义一个委托
public delegate void MyDelegate();
//定义一个事件
public MyDelegate MyDelegateEvent;
//定义一个触发事件的函数
public void OnMyDelegateEvent()
{
//判断事件是否非空
if (MyDelegateEvent != null)
{
//执行事件
MyDelegateEvent();
}
//MyDelegateEvent?.Invoke(); //简化的判断和执行
}
}
class Program
{
//输出一串字符
public static void putOutChar()
{
Console.WriteLine("I was fired");
}
//输出第二串字符
public static void putOutChar2()
{
Console.WriteLine("I was fired22222");
}
static void Main(string[] args)
{
//实例化MyEvent2类
MyEvent myEvent = new MyEvent();
//注册一个事件
myEvent.MyDelegateEvent += new MyEvent.MyDelegate(putOutChar);
myEvent.MyDelegateEvent += new MyEvent.MyDelegate(putOutChar2);
//执行触发事件的函数
Console.WriteLine("执行绑定了两个事件后的函数");
myEvent.OnMyDelegateEvent();
//解绑一个事件
myEvent.MyDelegateEvent -= new MyEvent.MyDelegate(putOutChar);
//再次执行触发事件的函数
Console.WriteLine("执行解绑了一个事件后的函数");
myEvent.OnMyDelegateEvent();
Console.ReadKey();
}
}
}
**C#中的事件其实可以理解为一个特殊的多播委托。**事件分为两个阶段一个是委托的实例化(对应事件订阅),一个是委托的调用(对应事件触发)。
6、泛型
C# 泛型和 C++ 模板都是用于提供参数化类型支持的语言功能。 然而,这两者之间存在许多差异。 在语法层面上,C# 泛型是实现参数化类型的更简单方法,不具有 C++ 模板的复杂性。 此外,C# 并不尝试提供 C++ 模板所提供的所有功能。 在实现层面,主要区别在于,C# 泛型类型替换是在运行时执行的,从而为实例化的对象保留了泛型类型信息。C++的模板可以认为是C#泛型的超集。
泛型委托
可以通过类型参数定义泛型委托。例如:
delegate T NumberChanger<T>(T n);
下面的实例演示了委托的使用:
using System;
using System.Collections.Generic;
delegate T NumberChanger<T>(T n);
namespace GenericDelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 创建委托实例
NumberChanger<int> nc1 = new NumberChanger<int>(AddNum);
NumberChanger<int> nc2 = new NumberChanger<int>(MultNum);
// 使用委托对象调用方法
nc1(25);
Console.WriteLine("Value of Num: {0}", getNum());
nc2(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of Num: 35
Value of Num: 175
7、多线程
通过一个实例来认识一下C#的多线程;
using System;
using System.Threading;
namespace MultithreadingApplication
{
class ThreadCreationProgram
{
public static void CallToChildThread()
{
try
{
Console.WriteLine("Child thread starts");
// 计数到 10
for (int counter = 0; counter <= 10; counter++)
{
Thread.Sleep(500);
Console.WriteLine(counter);
}
Console.WriteLine("Child Thread Completed");
}
catch (ThreadAbortException e)
{
Console.WriteLine("Thread Abort Exception");
}
finally
{
Console.WriteLine("Couldn't catch the Thread Exception");
}
}
static void Main(string[] args)
{
ThreadStart childref = new ThreadStart(CallToChildThread);
Console.WriteLine("In Main: Creating the Child thread");
Thread childThread = new Thread(childref);
childThread.Start();
// 停止主线程一段时间
Thread.Sleep(2000);
// 现在中止子线程
Console.WriteLine("In Main: Aborting the Child thread");
childThread.Abort();
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
In Main: Creating the Child thread
Child thread starts
0
1
2
In Main: Aborting the Child thread
Thread Abort Exception
Couldn't catch the Thread Exception
带参数的线程函数
下面的例子介绍的是一个简单的方式,通过start传入参数,但这里只能传入一个object的类型参数;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace AAAAAA
{
class AAA
{
public static void Main()
{
Thread t = new Thread(new ParameterizedThreadStart(B));
t.Start("B");
Console.Read();
}
private static void B(object obj)
{
Console.WriteLine("Method {0}!",obj.ToString ());
}
}
}
除了上面的方法之外还可以利用lamda表达式的的方式来调用:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace AAAAAA
{
class AAA
{
public static void Main()
{
Thread t = new Thread(() => B("bbb"));
t.Start();
Console.Read();
}
private static void B(string obj)
{
Console.WriteLine("Method {0}!",obj.ToString ());
}
}
}
8、Lambda表达式
“Lambda 表达式”是采用以下任意一种形式的表达式:
-
表达式为其主体:
C#复制
(input-parameters) => expression -
语句块作为其主体:
C#复制
(input-parameters) => { <sequence-of-statements> }
使用 lambda 声明运算符=>从其主体中分离 lambda 参数列表。 若要创建 Lambda 表达式,需要在 Lambda 运算符左侧指定输入参数(如果有),然后在另一侧输入表达式或语句块。任何 Lambda 表达式都可以转换为委托类型。 Lambda 表达式可以转换的委托类型由其参数和返回值的类型定义。收先介绍表达式为主体的:
在Lambda表达式中,输入参数是Lambda运算符的左边部分。它包含参数的数量可以为0、1或者多个。只有当输入参数为1时,Lambda表达式左边的一对小括弧才可以省略。输入参数的数量大于或者等于2时,Lambda表达式左边的一对小括弧中的多个参数质检使用逗号(,)分割。
- 下面创建一个Lambda表达式,它的输入参数的数量为0.该表达式将显示“This is a Lambda expression”字符串。
()=>Console.WriteLine("This is a Lambda expression.");
- 下面创建一个Lambda表达式,它的输入参数包含一个参数:m。该表达式将计算m参数与2的乘积。
m=>m*2;
上述Lambda表达式的输入参数省略了一对小括弧,它与“(m)=>m*2”Lambda表达式是等效的。
- 下面创建一个Lambda表达式,它的输入参数包含两个参数:m和n。该表达式将计算m和n参数的乘积。
(m,n)=>m*n;
语句块做主体:
多个Lambda表达式可以构成Lambda语句块。语句块可以放到运算符的右边,作为Lambda的主体。语句块中可以包含多条语句,并且可以包含循环、方法调用和if语句等。
- 下面创建一个Lambda表达式,它的输入参数包括两个参数:m和n。该表达式的右边包含2个表达式;第一个表达式计算m和n参数的乘积,结果保存为result变量;第二个表达式显示result变量的值。
(m,n)=>{int result=m*n; Console.WriteLine(result);}
上述Lambda表达式的右边部分包含2个表达式,因此,该表达式的右边部分必须被"{"和"}"包围。
四、C#封装C语言接口
概述
对于实际的开发来说,可能需要对在C#编程中使用C++或C语言封装的接口,实际一点就是用C#去调C语言封装出来的库(主要是.dII文件),本节在于表述如何根据一个C/C++的库以及头文件进行C#的编程。 首先需要明确的是Dllimport可以对C语言或C++的接口进行封装,但由于托管和非托管方法的不同,Dllimport并不支持对所有C++代码段的封装,比如类的结构,因此可以将C++的代码先行封装出一个C语言的库或者头文件,之后便可以对C语言的库进行完整的封装。C语言的封装方式这里不做阐述。
托管VS非托管
一句话简要介绍:所谓托管是指内存管理由系统来做而不是程序员。 重点其实还是要落在C++上,这门语言不像C#,C#是典型的托管型代码,它的内存管理(内存的分配和释放)都是由系统管理的。所以只有new而没有delete。C++有个很重要的特点就是内存由程序员管理。所以分配内存以后,要程序员自己释放。如果没有释放就会有内存泄露,如果在不该释放时释放了,就会出现野指针。托管C++是微软所创造一种扩展C++的一种叫法。英文写出来是Managed C++,就更能看出宣传有系统管理内存这个意思。所以后来微软推出了C++/CLI,相对Managed C++对标准的C++做了很多更激进的改造。当然C++/CLI依然有系统(准确的说是.net框架)管理内存功能,所以也有人把C++/CLI依然称为托管C++.
上面的废话又多了,重点在这里:你可以简单的把托管C++看成在语言层次上支持.net框架。我们之所以要了解托管就是为了这个:支持.Net框架。这样我们通过一种包装的方式就把C++程序放到了.Net平台上。不过这部分仅作为了解,本篇文章仅介绍Dllimport方法,托管C++这种方式不做介绍。
C#dllimport方式
命名空间
using System.Runtime.Interopservices;
Dllimport简单实现
详细介绍之前先介绍最简单的使用方法:
[DLLlmport(“DLL文件路经”)] 修饰符 extern 返回值类型 方法名称(参数列表)
其中: DLL文件:包含定义外部方法的库文件。 修饰符:访问修饰符,除了abstract以外在声明方法时可以使用的修饰符。 返回变量类型:在DLL文件中你需调用方法的返回变量类型。 方法名称:在DLL文件中你需调用方法的名称。 参数列表:在DLL文件中你需调用方法的列表 例子如下 C语言:
extern "C” _declspec(d]lexport) int WINAPI func( int i,int y)
{
int a = i;//传进来的值
*y=9;//可以更改外部的值
return a;
}
C#:
[DllImport("D11Sample.d11")]
static extern int func( int id, ref int y);
int i-1;
int y ;
int result = SetprocessInfo(i,ref y);
可以看到通过上面的方式就将C语言的接口放到了C#这边,后续便可以直接调用这个函数,并且,这个函数的实现实在C语言端,C#仅有函数接口。
上面的篇幅很长,推荐大家的学习还是从简单的项目学起,这里相比较教学更多的总结归纳,大家可以学习一些简单的C#项目,我本人也是在学习中,后面会再分享