操作系统中的输入输出

输入输出 I/O硬件: I/O设备分为两类:块设备和字符设备,块设备吧信息存储在固定大小的块中,每个块有自己的地址,传输以块为单位,每个块都能独立于其他块读写,硬盘,CD-ROM和USB盘都是常见的块设备。字符设备是以字符为单位发送和接收一个字符流,而不考虑任何块结构,字符设备不可寻址,也不寻道,打印机,网络几口,鼠标,以及大多数与磁盘不同的设备都可看作是字符设备。 I/O设备一般由机械部件和电子部件两部分组成,通常分开处理,实现模块化和通用设计,电子部件称作设备控制器/适配器,在个人计算机上,通常以主板上的芯片的形式出现,或者以插入PCI的印刷电路板的形式出现。控制器卡上通常有一个连接器,通向设备本身的电缆可以插入到这个连接器中, 控制器的任务是吧串行的位流转换成字节块,并进行必要的错误校正工作,字节块通常首先在控制器内部的一个缓冲区中按位进行组装,然后再对校验和进行校验并证明字节块没有错误后再将它复制到主存中。 每个控制器都有几个寄存器用来和cpu通信,通过写入这些寄存器,操作系统可以命令设备发送数据等等操作。 1.内存映射io 将所有控制寄存器映射到内存空间中,每个寄存器被分配一个唯一的内存地址,并且不会有内存被分配这一地址,这样的系统称为内存映射I/O,通常位于地址空间的顶端。使用内存映射io,设备控制器只是内存中的变量,c语言可以和其他变量一样寻址,这样,I/O设备驱动程序就可以采用c语言编写。 2.DMA 无论CPU是否具有内存映射I/O,他都需要寻址设备控制器以便和他们交换数据,但浪费eficpu时间,所以经常使用直接存储器存储。可独立于cpu访问地址总线。 没有DMA的时候,首先控制器从磁盘驱动器串行的一位一位的读一个块,直到将整块信息放入控制器的内存缓冲区中,接着,他计算校验和,以保证没有读错误发生,然后控制器产生一个中断,当操作系统开始运行时,它重复地从控制器的缓冲区中一次一个字节/一个字的读取该块的信息,并将其放入内存中。 当有DMA的时候,首先CPU通过设置DMA控制器的寄存器对它进行编程,所以DMA控制器知道将什么数据传送到什么地方,(第1步)DMA控制器还要向磁盘控制器发送一个命令,通知他从磁盘读数据到其内部的缓冲区中,并且对校验和进行检验,如果磁盘控制器中的缓冲区中的数据是有效的的。那么DMA开始 DMA控制器通过在总线上发出一个读请求到磁盘控制器而发起DMA传送(第2步),这一读请求和其他一样,并且磁盘控制器并不关心是来自DMA还是CPU,一般情况下,要写的内存地址在总线的地址线上,所以磁盘控制器从内部缓冲区中读取下一个字的时候,她知道要写的什么地方,写到内存是另一个标准总线周期,(第3步) 当写操作完成时,磁盘控制器在总线上发起一个应答信号到DMA(第4步),于是DMA控制器部增要使用的内存地址,并且步减字节计数,如果字节计数仍然大于0,则从父2-4步。完成后产生中断告诉cpu,操作系统开始工作时,数据已经在内存中了。 中断: 将机器留在一个明确状态的中断称为精确中断,四个特征,1.PC保存在一个已知的地方。2.PC所指向的指令之前的所有指令都已经完全执行。3.PC所指向的指令之后的所有指令都没有执行。4.PC所指向的指令的执行状态是已知的。注意,对于PC所指向的指令以后的指令,并没有禁止他们开始执行,而只是要求在中断发生之前必须撤销他们对寄存器或内存所做的任何修改。 I/O软件: 设计I/O软件时一个关键的点就是设备独立性,意思是我们可以访问任意I/O设备而无需事先指定设备。也就是对于不同的I/O硬件。同一段程序是可以的。 具有标准接口的驱动程序的工作方式如下:对于每一种设备类型,例如磁盘和打印机。操作系统定义一组驱动程序必须支持的函数,对于磁盘而言,这些函数自然的包含读和写,除此之外还包含开启和关闭电源,格式化以及其他与磁盘有关的事情。驱动程序通常包含一张表格,这张表格具有针对这些函数指向驱动程序自身的指针。当驱动程序装载时,操作系统记录下这张函数指针表的地址。所以当操作系统需要调用一个函数时,可以通过表格发出间接调用。这张函数指针表定义了驱动程序与操作系统其他部分之间的接口。 **双缓冲:**当第二个缓冲区正在复制用户空间的时候,第一个缓冲区用来接收新的字符。以这样的方法。两个缓冲区轮流使用。称为双缓冲。 磁盘臂调度算法: 读/写一个磁盘块需要时间:1.寻道时间(将磁盘臂移动到适当的柱面上所需的时间)2.旋转延迟(等待适当扇区旋转到磁头下所需的时间)。3.实际数据传输时间。 一个磁盘子系统具有如下特性:当一个写命令发给它时,磁盘要么正确地写数据,要么什么也不做,让现有的数据完整无缺的留下,这样的系统称为稳定存储器,并且是在软件中实现的。目标是不惜一切代价保持磁盘的一致性。 **时钟:**两种。1种是直接接到电源线上。就可以每个电压周期产生一个终端。现在比较少。另一种是由晶体振荡器,计数器和存储寄存器三个构成。当把一块石英晶体适当的切割并且安装到一定的压力之下时就可以产生非常精确的周期性信号。时钟启动时,存储寄存器的值被复制到计数器中,每一个脉冲使计数器-1,直到为0,产生中断。

2013-02-01 · 1 min · bystander

操作系统中的文件系统

文件系统 进程,地址空间,文件这些抽象概念均是操作系统中的重要概念,如果理解了这三个概念,就迈上了成为一个操作系统专家的道路。 文件系统存放在磁盘上,多数磁盘划分为一个/多个分区,每个分区有一个独立的文件系统,磁盘的0号扇区称为主引导记录,也就是MBR,用来引导计算机,MBR的结尾就是分区表了。该表给出了每个分区的起始和结束地址。表中的一个分区被标记为活动分区。在计算机被引导时,BIOS读入并执行MBR,MBR做的第一件事就是确定活动分区,读入他的第一个块,称为引导块,并执行之,引导块中的程度将装载该分区中的操作系统,为统一起见,每个分区都从一个启动块开始,即使它不含有一个可以启动的操作系统。 文件的实现: 1.连续分配,每个文件作为一连串连续数据存储在磁盘上。实现简单,读操作性能好,一次就可以了。但不足是删除之后不能移动,因为成本太高,使得空块增多。碎片化严重。更诡异的是对于文件编辑软件,实现无法准确预测大小,如果预测错了。。就跪了。 //研究那些具有清晰和简洁概念的老式系统和思想是很重要的,因为他们可能以一种令人吃惊的方式在未来系统中获得应用。 2.链表分配 为每个文件构造磁盘块链表,一个文件分为N个文件块,N个文件块构成一个链表,存储在物理上的多个地方。顺序读取很方便,但随机读取则相当缓慢,由于指针的存在,每个磁盘块存储数据的字节不再是2的整数次幂,导致系统运行效率降低,因为很多程序都是以2的整数次幂来读写磁盘的。 3.在内存中采用表的链表分配 去除每个文件块在磁盘上的指针字,放入内存的一个表上,就可以解决上一个分配的不足。直观的例子如图。 文件A使用了磁盘块4,7,2,10,12 内存中这样的表格称为文件分配表,也就是FAT了。主要缺点是对于大磁盘的小块,这种表需要的内存占用太大。。不太适用。 4.i节点 记录各个文件包含哪些磁盘块的方法是给每个文件赋予一个称为i节点的数据结构,其中类除了文件属性和文件块的磁盘地址.相对于在内存中采用表的方式,这种机制的优势在于只有对应文件打开时,其i节点才进入内存. 文件系统的一致性检查分为两种:块的一致性检查和文件的一致性检查.构造两张表,一张跟踪块在文件中的出现次数,另一张跟踪该块在空闲表中的出现次数,如果一致,则某一块必然在两个表中1/2中为1,如果某一块没有出现在任何一张表中,则称为块丢失,浪费了磁盘空间.解决方法是让文件系统检验程序把他们加入到空闲表中 如果在空闲表中出现了两次.则重新建议建议空闲表即可. 如果在文件表中出现了两次.则比较麻烦. 文件系统性能 1.高速缓存,最常用,指的是一系列的块,逻辑上属于磁盘.但实际上被保存在内存上.基本算法是检查全部的读请求,查看在高速缓存中是否有所需要的块,如果存在,就读,否则读入高速缓存在复制到其他地方. 2.块提前读,在需要用到块之前,试图提前将其写入高速缓存,从而提高命中率.比如某个文件有n个块,则请求k块的时候,则同时预读k+1块.只适用于顺序读取的文件,对随机读取文件,则没有效果/反效果. 3.减少磁盘臂运动 把所有可能顺序读取的块放在一起,当然最好是放在同一个柱面上,从而减少磁盘臂的移动次数.

2013-01-31 · 1 min · bystander

现代操作系统的调度

这几天在读《现代操作系统》,想起当时学这门课的时候,并没有感觉那么爽,现在通读这本书,知识的过渡性和结构性令我叹服。感受操作系统的魅力吧。 批处理系统中的调度: 1.先来先服务 2.最短作业优先 只有在所有的作业都可以同时运行(也即同时到达)的情况下,最短作业优先算法才是最优化的。 3.最短剩余时间优先-最短作业优先的抢占式版本。调度算法总是选择剩余时间最短的那个进程运行,注意,运行时间必须提前掌握,当一个新的作业到达时,其整个时间同当前进程的剩余时间做比较,如果更少。就运行新进程。可以使新的短作业获得良好的服务。 交互式系统的调度 1.轮转调度。 最古老,最简单,最公平切使用最广,每个进程被分配一个时间片。如果进程在时间片结束之前阻塞或结束,则CPU立即切换。调度程序只是维护一张可运行进程列表,当进程用完它的时间片后,就被移到队列的末尾。时间片太短会导致进程切换过多,降低CPU效率,设置的太长又引起对短的交互请求的响应时间变长。通常20-50ms算合理。 2.优先级调度 为了防止高优先级进程无休止的运行下去,可以在一个时钟中断里降低当前进程的优先级,如果这导致该进程的优先级低于次高优先级的进程,则切换或者也可以赋予每个进程一个时间片。可以和轮转调度一起工作,设置每个优先级上有多个进程。优先运行高优先级,并未高优先级上的进程按照轮转换着运行,如果高优先级没了。就进入到较低优先级。。。问题是如果不偶尔对优先级进行调整,则可能发生饥饿现象。 3.多级队列 CTSS的设计者发现为CPU密集型进程设置较长的时间片比频繁的分给他们很短的时间片更为高效(减少了交换次数),但长时间的进程又会影响响应时间,方法是设立优先级类,最高优先级类里的进程运行1个时间片。次高运行2个。以此类推。当一个进程用完分配的时间片后,被移动到下一类。大致算法都是用于讨好交互用户和进程,而不惜牺牲后台进程 //故事:可以采用只要终端上有Enter键按下,就将该终端上的进程移到最高优先级类。假设当前进程急需交互,但是。一个人发现了。大家都开始用。。。理论和实际差异太大。。哈哈 4.最短进程优先 这个很好立即,但难点在于如何找出最短的那个。一种方法是根据过去的行为推测。假设每个命令执行时间为T0,下一次运行时间为T1,则可以根据aT0+(1-a)T1来估计时间。。a被用来决定尽快忘掉老的运行时间还是记住它。这种算法成为老化算法。通常选a=1/2 5.保证调度 就是保证每个用户获得cpu的1/n,系统需要跟踪进程的cpu时间,他实际获得如果多于应该获得的。则转向实际获得小于应该获得的。 6.彩票调度 保证调度很难实现,而彩票调度算法是向进程提供各种系统资源的彩票。一旦需要做出一项调度决策时,就随机抽出一张彩票。谁获得谁就上。比如视频服务器,可以为不同的帧速率提供不同的彩票。然后分配cpu 7.公平分享调度 这个就考虑到了进程的所有者。需要我们定义公平的含义。是保证每个用户只占用的时间相等还是其他了。 实时系统的调度: 可以分为硬实时和软实时,前者必须满足绝对的截止时间,后者则可以容忍一些。用户级线程系统是不知道的。用户级和内核级的差异主要在性能,用户级需少量的机器指令,而内核级需要很多的。过程。采用轮转和优先级调度更常见一些。 //操作系统的大神们太强大了。哲学家进餐问题居然可以通过拿起左边叉子以后,检测右边是否可用,如果不可用,则等待一个随机的时间。这种方案是可行的。在极少的情况下不可用。。

2013-01-24 · 1 min · bystander

图的遍历(C#)

讲的非常好的一篇文章。感谢abatei,直接收藏分享之。 图的存储结构 图的存储结构除了要存储图中各个顶点的本身的信息外,同时还要存储顶点与顶点之间的所有关系(边的信息),因此,图的结构比较复杂,很难以数据元素在存储区中的物理位置来表示元素之间的关系,但也正是由于其任意的特性,故物理表示方法很多。常用的图的存储结构有邻接矩阵、邻接表、十字链表和邻接多重表。 8.2.1 邻接矩阵表示法 对于一个具有n个顶点的图,可以使用n*n的矩阵(二维数组)来表示它们间的邻接关系。图8.10和图8.11中,矩阵A(i,j)=1表示图中存在一条边(Vi,Vj),而A(i,j)=0表示图中不存在边(Vi,Vj)。实际编程时,当图为不带权图时,可以在二维数组中存放bool值,A(i,j)=true表示存在边(Vi,Vj),A(i,j)=false表示不存在边(Vi,Vj);当图带权值时,则可以直接在二维数组中存放权值,A(i,j)=null表示不存在边(Vi,Vj)。 图8.10所示的是无向图的邻接矩阵表示法,可以观察到,矩阵延对角线对称,即A(i,j)= A(j,i)。无向图邻接矩阵的第i行或第i列非零元素的个数其实就是第i个顶点的度。这表示无向图邻接矩阵存在一定的数据冗余。 图8.11所示的是有向图邻接矩阵表示法,矩阵并不延对角线对称,A(i,j)=1表示顶点Vi邻接到顶点Vj;A(j,i)=1则表示顶点Vi邻接自顶点Vj。两者并不象无向图邻接矩阵那样表示相同的意思。有向图邻接矩阵的第i行非零元素的个数其实就是第i个顶点的出度,而第i列非零元素的个数是第i个顶点的入度,即第i个顶点的度是第i行和第i列非零元素个数之和。 由于存在n个顶点的图需要n2个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将出现大量零元素,照成极大地空间浪费,这时应该使用邻接表表示法存储图中的数据。 8.2.2 邻接表表示法 图的邻接矩阵存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。邻接表由表头结点和表结点两部分组成,其中图中每个顶点均对应一个存储在数组中的表头结点。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。如图8.12所示,表结点存放的是邻接顶点在数组中的索引。对于无向图来说,使用邻接表进行存储也会出现数据冗余,表头结点A所指链表中存在一个指向C的表结点的同时,表头结点C所指链表也会存在一个指向A的表结点。 有向图的邻接表有出边表和入边表(又称逆邻接表)之分。出边表的表结点存放的是从表头结点出发的有向边所指的尾顶点;入边表的表结点存放的则是指向表头结点的某个头顶点。如图8.13所示,图(b)和(c)分别为有向图(a)的出边表和入边表。 以上所讨论的邻接表所表示的都是不带权的图,如果要表示带权图,可以在表结点中增加一个存放权的字段,其效果如图8.14所示。 【注意】:观察图8.14可以发现,当删除存储表头结点的数组中的某一元素,有可能使部分表头结点索引号的改变,从而导致大面积修改表结点的情况发生。可以在表结点中直接存放指向表头结点的指针以解决这个问题(在链表中存放类实例即是存放指针,但必须要保证表头结点是类而不是结构体)。在实际创建邻接表时,甚至可以使用链表代替数组存放表头结点或使用顺序表存代替链表存放表结点。对所学的数据结构知识应当根据实际情况及所使用语言的特点灵活应用,切不可生搬硬套。 【例8-1 AdjacencyList.cs】图的邻接表存储结构 using System; using System.Collections.Generic; public class AdjacencyList<T> { List<Vertex<T>> items; //图的顶点集合 public AdjacencyList() : this(10) { } //构造方法 public AdjacencyList(int capacity) //指定容量的构造方法 { items = new List<Vertex<T>>(capacity); } public void AddVertex(T item) //添加一个顶点 { //不允许插入重复值 if (Contains(item)) { throw new ArgumentException("插入了重复顶点!"); } items.Add(new Vertex<T>(item)); } public void AddEdge(T from, T to) //添加无向边 { Vertex<T> fromVer = Find(from); //找到起始顶点 if (fromVer == null) { throw new ArgumentException("头顶点并不存在!"); } Vertex<T> toVer = Find(to); //找到结束顶点 if (toVer == null) { throw new ArgumentException("尾顶点并不存在!"); } //无向边的两个顶点都需记录边信息 AddDirectedEdge(fromVer, toVer); AddDirectedEdge(toVer, fromVer); } public bool Contains(T item) //查找图中是否包含某项 { foreach (Vertex<T> v in items) { if (v.data.Equals(item)) { return true; } } return false; } private Vertex<T> Find(T item) //查找指定项并返回 { foreach (Vertex<T> v in items) { if (v.data.Equals(item)) { return v; } } return null; } //添加有向边 private void AddDirectedEdge(Vertex<T> fromVer, Vertex<T> toVer) { if (fromVer.firstEdge == null) //无邻接点时 { fromVer.firstEdge = new Node(toVer); } else { Node tmp, node = fromVer.firstEdge; do { //检查是否添加了重复边 if (node.adjvex.data.Equals(toVer.data)) { throw new ArgumentException("添加了重复的边!"); } tmp = node; node = node.next; } while (node != null); tmp.next = new Node(toVer); //添加到链表未尾 } } public override string ToString() //仅用于测试 { //打印每个节点和它的邻接点 string s = string.Empty; foreach (Vertex<T> v in items) { s += v.data.ToString() + ":"; if (v.firstEdge != null) { Node tmp = v.firstEdge; while (tmp != null) { s += tmp.adjvex.data.ToString(); tmp = tmp.next; } } s += "\r\n"; } return s; } //嵌套类,表示链表中的表结点 public class Node { public Vertex<T> adjvex; //邻接点域 public Node next; //下一个邻接点指针域 public Node(Vertex<T> value) { adjvex = value; } } //嵌套类,表示存放于数组中的表头结点 public class Vertex<TValue> { public TValue data; //数据 public Node firstEdge; //邻接点链表头指针 public Boolean visited; //访问标志,遍历时使用 public Vertex(TValue value) //构造方法 { data = value; } } } AdjacencyList<T>类使用泛型实现了图的邻接表存储结构。它包含两个内部类,Vertex<Tvalue>类(109~118行代码)用于表示一个表头结点,Node类(99~107)则用于表示表结点,其中存放着邻接点信息,用来表示表头结点的某条边。多个Node用next指针相连形成一个单链表,表头指针为Vertex类的firstEdge成员,表头结点所代表的顶点的所有边的信息均包含在链表内,其结构如图8.12所示。所不同之处在于: l Vertex类中包含了一个visited成员,它的作用是在图遍历时标识当前节点是否被访问过,这一点在稍后会讲到。 ...

2013-01-06 · 4 min · bystander

Lambda高手之路第四部分

首先祝大家平安夜快乐。本篇介绍一些流行的JavaScript模式。为下一篇打基础 使用/了解JavaScript的一个好处就是函数的高级用法。。在JavaScript里。函数仅仅是对象。他们可以有赋给他们的属性。而在C#中。我们不能做我们可以在JavaScript的全部事情。但是我们仍然可以做些事情。一个原因是JavaScript在函数里给变量以作用域。因此,不得不通过创建函数,大多数情况是匿名的来定位变量。而在C#中。通过使用块,通过花括号来创建作用域 当然,换种方式来说。C#中,函数也会给变量作用域。通过使用Lambda表达式。我们通过花括号在其里面创建了一个变量。然而。我们也可以局部的创建作用域。 我们来看看通过使用Lambda表达式可以实现一些在JavaScript里面有用的模式把。 回调模式 这个模式是个老的模式。事实上。回调模式从.net 的第一版就开始使用了。但是是以一种很简单的方式实现的。而现在。通过使用Lambda表达式。闭包,捕获变量等特性能够允许我们写出如下的代码来。 void CreateTextBox() { var tb = new TextBox(); tb.IsReadOnly = true; tb.Text = "Please wait ..."; DoSomeStuff(() => { tb.Text = string.Empty; tb.IsReadOnly = false; }); } void DoSomeStuff(Action callback) { // Do some stuff - asynchronous would be helpful ... callback(); } 对于JavaScript程序员会觉得这没什么啊。他们使用这个模式太多了。然而,它非常有用。因为我们可以使用参数作为Ajax相关事件的事件处理器(比如oncompleted,onsuccess),等等。如果你使用LINQ,那么你可能也会用到回调模式的一些东西。举个例子。LINQ的where子句将会在每一次迭代中回调你的查询语句。这只是回调函数的一个例子。在.net的世界里。事件如它名字所暗示的那样。通常是事件处理的首选方法。这有时候很像一个回调。他有两个参数。有一个特殊的关键字和一个类型模式(两个参数分别是sender和arguments,sender通常是object类型。Arguments通常继承自EventArgs) 可以通过+= 和-=给事件添加/删除事件处理程序。 返回方法 和普通的方法比较。Lambda表达式也可以返回一个方法指针(就是一个委托实例),这意味着我们可以使用Lambda表达式创建/返回一个lambda表达式(或者今年仅是一个已定义好的方法的委托实例),大量的情况下。这个模式也很有用。首先看一下例子。 Func<string, string> SayMyName(string language) { switch(language.ToLower()) { case "fr": return name => { return "Je m'appelle " + name + "."; }; case "de": return name => { return "Mein Name ist " + name + "."; }; default: return name => { return "My name is " + name + "."; }; } } void Main() { var lang = "de"; //Get language - e.g. by current OS settings var smn = SayMyName(lang); var name = Console.ReadLine(); var sentence = smn(name); Console.WriteLine(sentence); } 代码本应该更短些。我们可以让default如果请求的语言没有找到。只是抛出一个异常即可。不过。这个例子展示了这是一种方法工厂。另一种同等效果的方法是包含一个Hashtable。或者更好的话用Dictionary<K, V> ...

2012-12-24 · 3 min · bystander

Lambda高手之路第一部分

好长时间没发技术文章了,恰好看到一篇非常详细的Lambda文章。一边翻译一边学习。题目好像有点霸气。。 介绍 Lambda表达式是使代码更加动态,易于扩展并且更加快速(看完本文你就知道原因了)的强有力的工具。也可以用来降低潜在的错误。同时可以利用静态输入和智能提示,就像VS里一样。 Lambda表达式在.net framework 3.5中提出来。并且在LINQ和ASP.NET MVC内部的一些技术中扮演了相当重要的角色。如果你考虑一下ASP.NET MVC中各类控件的实现。你就发现。奥妙就是他们大多使用了Lambda表达式。和Lambda表达式一起,使用Html扩展方法将会使得在后台创建模型成为可能。 本文会讲到如下的知识。 1.简短的介绍-Lambda表达式是什么,以及为什么和匿名方法不同(之前我们使用的) 2.走近Lambda表达式的性能-在哪些情况下比起标准方法,Lambda会提高/损失性能 3.深入-Lambda表达式在MSIL代码中是什么样 4.一些来自JS世界的模式映射到C#中 5.那些能够提高性能,并且代码看起来相当舒服的使用Lambda的情况。 6.一些我提出的新模式-当然有可能别人也提出来了。但这是我的思考结果。 如果你期望本文是一篇入门教程我可能要让你失望了,除非你真的很优秀并且很聪明,当然我不是这种人,所以我也想提前声明一下:为了读懂这篇文章你可能需要C#的一些高级知识,并且对C#比较了解。 你应该期望本文试着解释一些事情给你,也会解释一些有趣的问题,至少对我来说是这样的。最后我会展示一些实际的例子和模式,如我所说,Lambda表达式简化了很多情况。因此写显式的模式很有用。 背景知识-什么是Lambda表达式 在C#1.0中,委托被提出了,它使得传递函数成为可能,一句话就是委托就是强类型的函数指针,但委托比指针更强大。一般传递一个函数需要如下几步。 1. 写一个委托(就像一个类)包含返回类型和参数类型 2. 使用委托作为某一个函数的参数类型,这样,该函数就可以接受和委托描述的有着相同签名的函数了 3. 将一个委托类型的函数传递给委托,创建一个委托实例。 如果听起来很复杂,确实本来很复杂,但这是必需的。(虽然不是造火箭,但是比你认为的要更多的代码),然而步骤三不是必需的,编译器会为你做他,但是步骤1和2却是必不可少的。 幸运的是C#2.0出现了泛型,现在我们也可以写泛型类,方法,更重要的是,泛型委托,然而,直到.net framework 3.5的时候。微软意识到实际上只有两种泛型委托(当然有一些不同的重载),会覆盖99%的使用情况: 1.Action 没有任何输入参数,也没有输出参数。 2.Action<t1,…t16> 需要1-16个参数,没有输出参数。 3.Func<t1….t16,tout>需要0-16个参数,一个输出参数 Action和其对应的泛型版本(仅仅是一个动作,执行一些事情)返回void的时候。Func则可以返回最后一个参数指定的类型,通过这两个委托类型,我们事实上,大部分情况下。前面提到的三步中的第一部就不用写的。而第二步仍然需要。 那么如果我们想要运行代码的时候怎么做呢。在C#2.0中问题已经可以解决了。在这个版本里。我们可以创建委托方法,也就是一个匿名方法,然后这个语法一直未能流行起来,一个相当简化的匿名方法的版本类似这样: Func<double, double> square = delegate (double x) { return x * x; } 为了提高这种语法,欢迎来到Lambda表达式的国度。首先,这个Lambda名字怎么来的?事实上。来自于数学上的λ演算,更准确的说他是数学中一个正式的系统。用于通过变量绑定和替换来进行表达式计算,所以我们有0-N个输入参数和一个返回值,而在编程中,也可以没有返回值 我们看一下Lambda表达式的一些例子 //编译器可以识别,然后就可以通过dummyLambda();来调用了 var dummyLambda = () => { Console.WriteLine("Hallo World from a Lambda expression!"); }; //可以通过类似 double y = square(25);来使用 Func<double, double> square = x => x * x; //可以通过类似double z = product(9, 5);来使用 Func<double, double,double> product = (x, y) => x * y; //可以通过类似printProduct(9, 5);来使用 Action<double, double> printProduct = (x, y) => { Console.Writeline(x * y); }; //可以通过类似var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 }); Func<double[], double[], double> dotProduct = (x, y) => { var dim = Math.Min(x.Length, y.Length); var sum = 0.0; for(var i = 0; i != dim; i++) sum += x[i] + y[i]; return sum; }; //可以通过类似 var result = matrixVectorProductAsync(...);使用 Func<double[,], double[], double[]=""> matrixVectorProductAsync = async (x, y) => { var sum = 0.0; /* do some stuff ... */ return sum; }; 从上面的代码段里我们可以学到一些东西 ...

2012-12-18 · 2 min · bystander

[源码]打包下载算法与数据结构演示动画

很早的时候,学习数据结构的时候。收集了一下演示的动画。帮助理解。但是不全。今天在看KMP算法的时候。看到了福州大学的一个精品课程。。81个演示动画呢。。想打包下载收藏。话说福州大学这才是好样的。踏踏实实搞学术。 第一种方法就是手工了。。嘎嘎。你敢么。一个个下载。。。一个个改名。。 第二种就是用整站下载的软件了。。但是我看了一下swf的命名。我就知道下载下来意义不大。因为名字不好理解。 第三种就是自己写个程序吧。。 整体思路,首先访问课程页面,解析得到每一章的标题和内容,然后创立章节文件夹,得到每个动画对应的html页面,然后对html页面解析,提取swf地址。然后下载就行了。 比较疼的地方是那个页面用的是gb2312编码。而解析神器HtmlAgilityPack,不能指定编码。只能想办法绕过了。 WebClient client = new WebClient(); MemoryStream ms = new MemoryStream(client.DownloadData(url)); HtmlDocument doc = new HtmlDocument(); doc.Load(ms, Encoding.GetEncoding("gb2312")); 绕过方法就是先使用内置类得到内存流。然后从内存中加载。 然后呢。涉及的技术就是xpath了。参考着xpath的文档。搞定了不少。中间还有一个地方就是我没注意看。这个页面有两个文件是一样名字。。调试了几次才发现。。 using System; using System.Collections.Generic; using System.Linq; using System.Text; using HtmlAgilityPack; using System.IO; using System.Threading; using System.Net; namespace FzuSwf { class Program { static void Main(string[] args) { DoWork(); } //执行任务 static void DoWork() { HtmlWeb web = new HtmlWeb(); HtmlDocument doc = web.Load("http://ds.fzu.edu.cn/fine/resources/"); HtmlNode divResource = doc.GetElementbyId("divResource"); foreach (HtmlNode child in divResource.ChildNodes) { if (child.Name == "table") { HtmlNode ptile = child.SelectSingleNode("tr[1]"); Directory.CreateDirectory(ptile.InnerText.Trim()); int i = 0; HtmlNodeCollection pcontents = child.SelectNodes("tr[position()>1]"); Console.WriteLine(ptile.InnerText.Trim()); foreach (HtmlNode one in pcontents) { string link = one.SelectSingleNode("./td[1]/p[1]/a[@href]").Attributes["href"].Value; link = @"http://ds.fzu.edu.cn/fine/resources/" + link; string filename; filename = one.InnerText.Trim(); if (one.InnerText.Trim() == "二叉树的顺序存储表示") { filename += i; i++; } string swfLink = getSwfName(link); Console.WriteLine("--" + filename + swfLink); DownSwf(swfLink, ptile.InnerText.Trim() + @"/" + filename + ".swf"); Thread.Sleep(1000); } } } } //下载指定的swf static void DownSwf(string url, string desname) { Uri u = new Uri(url); WebClient myWebClient = new WebClient(); myWebClient.DownloadFileAsync(u, desname); } //获取指定页面的那个swf名称 static string getSwfName(string url) { WebClient client = new WebClient(); MemoryStream ms = new MemoryStream(client.DownloadData(url)); HtmlDocument doc = new HtmlDocument(); doc.Load(ms, Encoding.GetEncoding("gb2312")); HtmlNode hd = doc.DocumentNode; string str = hd.SelectSingleNode("//a[@href]").Attributes["href"].Value; return @"http://ds.fzu.edu.cn/fine/resources/" + str; } } } ...

2012-12-03 · 1 min · bystander

C#模拟手工洗牌(附测试)

洗牌大家都知道,代码实现最广泛的一种就是产生两个随机数,然后交换这两个随机数为下标的牌,但是这种的洗牌并不能保证同概率,你可以参考本文做一些测试,本文代码没啥可说的。我写出了非常详细的注释 ps:刚开始写那个随机数的时候,我随便给了个种子2012.。结果你懂的。。意外意外。这个全局的result数组让我很疼,代码有什么可以改进的,欢迎留言指出。不胜感激。 /*Author:Bystander *Blog:http://leaver.me *Date:2012/11/24*/ using System; class Program { static int[,] result; static void Main() { //初始牌的顺序,我只测试10张牌的情况 char[] _arr = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' }; result = new int[_arr.Length, _arr.Length]; //进行1000次,来统计结果的数字. for (int i = 0; i < 10000; i++) { ShuffleArray_Manual(_arr); SumCount(_arr); } int j = 0; foreach (int i in result) { if (j % result.GetLength(1) == 0) { Console.WriteLine(); } Console.Write(i + "\t"); j++; } } //模拟洗牌 static void ShuffleArray_Manual(char[] arr) { Random rand = new Random(Guid.NewGuid().GetHashCode()); int len = arr.Length; int mid = len / 2; /* * 洗牌的过程重复进行5次,为了洗的均匀.每次都是先平分扑克,然后完全交叉,然后从牌中拿出一部分,放在牌的最前面,也就是切牌,然后再进行下次平分扑克。 */ //双手洗牌5次,默认认为是平分扑克 for (int n = 0; n < 5; n++) { //两手洗牌 for (int i = 1; i < mid; i += 2) { char tmp = arr[i]; arr[i] = arr[mid + i]; arr[mid + i] = tmp; } //随机切牌 //注意切牌指的是从中抽出n张牌放到扑克牌的前面去 char[] buf = new char[len]; for (int j = 0; j < 5; j++) { //产生从大于等于1小于len的数 int start = rand.Next() % (len - 1) + 1; //产生大于等于0小于等于一半的数 int numCards = rand.Next() % (len / 2) + 1; if (start + numCards > len) { numCards = len - start; } //把扑克牌arr的前start张牌复制到buf里 Array.ConstrainedCopy(arr, 0, buf, 0, start); ///然后把切出来的numCards张牌,起始下标为start的移动到扑克牌arr的最前面 Array.ConstrainedCopy(arr, start, arr, 0, numCards); ///最后把切出去的buf牌(numCards张)复制回扑克牌arr的numCards之后的元素 Array.ConstrainedCopy(buf, 0, arr, numCards, start); } } } //统计一次结果的次数,存入结果数组 static void SumCount(char[] arr) { for (int i = 0; i < arr.Length; i++) { result[arr[i] - 'A', i] += 1; } } }

2012-11-25 · 2 min · bystander

依赖倒置原则和依赖注入模式

昨天读完了程杰的《大话设计模式》。。收获颇丰。深刻感到了设计模式的伟大。。对面向接口的编程也理解了不少。刚好看到codeproject上一篇将依赖倒置的。讲到了依赖注入的方式。仔细读了一下。翻译一遍加深认识。 高耦合的代码随着项目复杂性的不断增加,最终会变成一碗碗的意大利面条啦。。二者通常是软件设计上的问题,如果一个类对另一个类的实现了解太多。当该类改变的时候会引起更多的改变。这违反了依赖倒置原则 而松耦合的代码设计优良。随着时间流逝,代码复杂两增大,松耦合的好处会变得更加清晰,依赖注入模式是实现松耦合的一个好的办法,本文介绍在没有依赖注入容器的情况下实现依赖注入 GoF说了,依赖倒置的原则: 高层模块不应依赖于低层模块,他们都应该依赖于抽象 抽象不依赖细节,细节依赖抽象 刚开始写依赖倒置比较难,随着经验增长会有所改善,通过使高层模块依赖于抽象,依赖倒置成功解耦,依赖注入模式是该原则的一个实现。 通常我们写出如下的代码: public class Email { public void SendEmail() { // code } } public class Notification { private Email _email; public Notification() { _email = new Email(); } public void PromotionalNotification() { _email.SendEmail(); } } Notification类依赖Email类,这违反了DIP,而且当我们要发送短信/保存到数据库的时候,我们还要改变Notification类。 我们使用抽象类/接口解耦 public interface IMessageService { void SendMessage(); } public class Email : IMessageService { public void SendMessage() { // code } } public class Notification { private IMessageService _iMessageService; public Notification() { _iMessageService = new Email(); } public void PromotionalNotification() { _iMessageService.SendMessage(); } } IMessageService 是一个接口,而Notification 类只要调用接口的方法/属性就可以了 同时,我们把Email对象的构造移到Notification 类外面去。 依赖注入模式可以实现。通常有三种方式 构造器注入 属性注入 方法注入 构造器注入 最普遍的方式,当一个类需要另一个类的依赖的时候,我们通过构造函数来提供,现在我们这样写 public class Notification { private IMessageService _iMessageService; public Notification(IMessageService _messageService) { this._iMessageService = _messageService; } public void PromotionalNotification() { _iMessageService.SendMessage(); } } 有几个好处:1.构造函数实现很简单,Notification类需要知道的很少。想要创建Notification实例的时候看构造函数就可以知道需要什么信息了。因此实现了松耦合。 属性注入 属性注入/setter注入比较不常见,当依赖可有可无的时候很有用。我们暴露一个可写的属性,允许客户提供不同的依赖实现,比如这样。 public class Notification { public IMessageService MessageService { get; set; } public void PromotionalNotification() { if (MessageService == null) { // some error message } else { MessageService.SendMessage(); } } } 没有了构造函数。而用属性来替换,在PromotionalNotifications 方法里我们需要检查MessageService的值或者提供相应的服务。 ...

2012-11-21 · 1 min · bystander

C#中的throw

Throw会抛出/传递异常,通过在catch块里使用throw语句.可以改变产生的异常,比如我们可以抛出一个新的异常,throw语句有各种各样的,并且很有必要. 例子 我们首先看一下三个方法,分别叫做A,B,C,他们使用不同的throw语句。方法A使用了无参的throw语句。这可以被看作是rethrow(继续抛出)—他会抛出已经出现的同样的异常 继续,方法B throw一个命名的异常变量。这就不是一个完全的rethrow了—因为他虽然抛出了同样的异常。但是改变了StackTrace(堆栈轨迹),如果有必要的话,我们可以收集一些异常信息,而方法C则创建了一个新的异常。 提示:你可以通过这种方法实现自定义的的错误处理 使用throw语句的例子 using System; class Program { static void Main() { try { A(); B(); C(null); } catch (Exception ex) { Console.WriteLine(ex); } } static void A() { // Rethrow 语法. try { int value = 1 / int.Parse("0"); } catch { throw; } } static void B() { // 过滤异常类型. try { int value = 1 / int.Parse("0"); } catch (DivideByZeroException ex) { throw ex; } } static void C(string value) { // 创建新的异常. if (value == null) { throw new ArgumentNullException("value"); } } } 程序可能的输出结果 System.DivideByZeroException: Attempted to divide by zero. System.DivideByZeroException: Attempted to divide by zero. System.ArgumentNullException: Value cannot be null. Parameter name: value Rethrow 接着我们看更多的关于rethrows的细节。Rethrow必须是一个无参的throw语句。如果使用throw ex,那么TargetSie(TargetSite 从堆栈跟踪中获取抛出该异常的方法。如果堆栈跟踪为空引用,TargetSite 也返回空引用。-译者注)和StackTrace都被改变了。 在下面的程序里,X()方法使用了rethrow语句。Y()使用了throw ex语句。我们可以看看当rethrow语句使用的使用,引发异常的方法,也就是异常的TargetSite是在StringToNumber—一个int.Parse内部的方法。 但是:当throw ex用的时候。就像在Y()里面,这个异常的TargetSite被修改到了当前的Y()方法里。 测试rethrow的例子 using System; class Program { static void Main() { try { X(); } catch (Exception ex) { Console.WriteLine(ex.TargetSite); } try { Y(); } catch (Exception ex) { Console.WriteLine(ex.TargetSite); } } static void X() { try { int.Parse("?"); } catch (Exception) { throw; // [Rethrow 构造] } } static void Y() { try { int.Parse("?"); } catch (Exception ex) { throw ex; // [Throw 捕获的ex变量] } } } 输出 ...

2012-11-18 · 1 min · bystander