视频游戏、社交网络和你的活动手环有什么共同点?它们运行在一群(或多或少)程序员在很远很远的地方编写的软件上。在我们这个技术驱动的社会中,小工具和硬件只是硬币更明显的一面。在这一章中,我们将讨论编程的基础知识。我们还将看看数字系统的可见部分:硬件。
基本上,编程是告诉数字设备,比如你的个人电脑,做什么的行为。我们键入由编程语言定义的命令列表,以便发生有用或有趣的事件。正确编程的计算机运行着世界上大部分的通信和在线服务。你可以提到像自动取款机、票阅读器和智能手机这样的小玩意,它们运行在某人用某种编程语言开发的软件上。
作为一名初露头角的程序员,你将从理解你正在使用的普遍存在的电子设备中受益。最好至少对计算机内部最常见的组件有一个基本的了解。
计算机中的这些硬件组件代表了你的劳动力。作为一名程序员,你来主持这个节目。把编程的行为想象成告诉工厂工人要造什么。你制造应用程序,无论它们是大型复杂的软件项目,还是一些令人敬畏的编程书籍中的教程。
出于本书的目的,任何相对现代的台式机或笔记本电脑都可以。在尝试编程时,我们不需要任何昂贵的硬件。
自然,一个数字设备不能只靠软件运行;一个中央处理器(CPU)是硬件“大脑”,它执行代码并使事情实际发生(见图1-1)。即使在一个不太复杂的电子产品中,所有指令都流向并通过一个CPU(或一组CPU)。由于体积非常小,自20世纪70年代以来,这些微芯片越来越成为我们生活的一部分。每个数字设备都有一个中央处理器,甚至可能是你的固定自行车/衣架。
图1-1
早在2005年,数百万台电脑中使用的旧英特尔奔腾4CPU的俯视图。图片由EricGaba提供。抄送-服务协议3.0
这个组件是用来永久存储数据的。在硬盘中,你会发现成千上万的文件,无论是图片、文本文件还是数据库。您的操作系统(如Windows或macOS)也在硬盘的范围内。这些设备有两种类型:机械硬盘(见图1-2)和固态硬盘(SSD)。
图1-2
机械驱动器更实惠,但由于它们内部有移动部件,因此在过度振动和极端天气下,它们比固态硬盘更容易损坏。此外,固态硬盘通常运行速度更快。
显卡负责显示系统的视觉效果,无论是纯文本还是现代视频游戏中令人眼花缭乱的3D图形。这些设备有多种配置和价格,从30美元的文字处理器恶魔到1000美元的游戏怪兽(见图1-3)。计算机显示器通常直接连接到视频卡。
图1-3
2006年的Nvidia7900GS显卡
自21世纪初以来,视频卡业务基本上一直是两家数十亿美元的科技巨头英伟达和AMD的垄断。然而,英特尔在这一领域也取得了进展。
随机存取存储器,俗称RAM,用作计算机的临时存储。在物理上,它通常以棒状附件的形式出现(见图1-4)。当运行任何类型的软件时,您的计算机使用RAM来执行它。关闭你的设备将清空你的内存。相比之下,当关闭电脑时,写在硬盘上的数据不会被擦除。定期保存您的文档。
图1-4
截至2021年,4GB(即4gb)对于大多数用途来说已经足够了。视频编辑等高级用户将受益于16GB或更大的RAM。
前面提到的所有四个硬件组件(即CPU、显卡、硬盘和RAM)在主板上组合在一起,形成一个工作的计算机单元。主板还具有用于键盘、鼠标和其他控制设备的连接器(参见图1-5)。
图1-5
接下来,让我们讨论一下所有程序员为了在他们的职业中进步而应该拥有的一些个人优先事项,无论他们的起点可能是什么:
输入在编程的上下文中,是指我们输入数据,让计算机上运行的一个软件进行处理。这以键入文本、鼠标命令或各种类型的文件的形式出现。例如,文字处理程序(例如,MicrosoftOffice)通常将其输入主要作为通过击键提供的字母数字数据。输出是指经过软件处理的数据。在字处理器中,这通常是指与程序一起保存的文件。这种输出也可以指向打印机或其他设备。程序员的输出(尽管有二氧化碳和其他东西)通常是一个工作应用程序,无论它是一个完整的教程文件还是一个更大的项目。
一个工作程序列表基本上构成了一个算法,指的是为解决问题而创建的一组步骤。大多数软件由许多子算法组成。在视频游戏的情况下,有显示图形、保存和加载游戏状态、播放音频文件等算法。
编程项目及其算法通常使用流程图来可视化,尤其是在团队环境中。在大多数情况下,这些都是演示基本程序流的好方法。
流程图仅由几个通用元素组成(见图1-6)。在它们最基本的形式中,它们使用四种符号。这些是终端(圆角矩形)流程(矩形)决策(菱形/菱形),以及流线(箭头)。结束符号用来表示程序流的开始和结束。任何操作和一般数据操作都由进程矩形表示。
图1-6
一个非常简单的描述愚人节程序的流程图
大多数情况下,流程图是从上到下、从左到右解释的。早在20世纪60年代,美国国家标准协会(ANSI)就创建了流程图及其符号的标准。这组符号在20世纪70年代和80年代被国际标准化组织(ISO)扩展到。为了这本书的目的,我们将坚持原著。
这个术语指的是组成每个软件项目的或多或少键入的编程清单的集合。作为程序员,你是源代码的创造者。简单的程序以单个源代码的形式出现,而复杂的软件,如操作系统(如Windows),可能由成千上万个清单组成,所有清单构成一个产品。
一个语法是一组规则和原则,它们管理给定语言中的句子结构,包括在编程环境中。不同的编程语言对特定的操作使用不同的关键字。现在,看看用两种编程语言显示文本字符串的实际编程行:
表1-1
两种编程语言之间语法差异的演示
爪哇
|
公式翻译程式语言(formulatranslator)
||---|---||System.out.print("你好!我喜欢蛋糕!”);|1打印*,“你好!我喜欢蛋糕!”|
Java,你可能已经知道了,是本书的主要语言之一。表1-1中使用的另一种编程语言叫做FORTRAN。这种语言主要是为科学计算而设计的,由IBM在20世纪50年代创造。许多工业硬件都在FORTRAN上运行。甚至一些极客仍然用它来追求技术时尚(在某种程度上,我们也是如此)。
你可能会注意到我们在表1-1中的一个例子是以数字(1)开始的。在编码术语中,这被称为行号,这种做法早就被放弃了。通常,当代编程语言不需要行号。
编程环境中的例程是一个代码术语,它完成一项特定的任务,并被编码者随意反复调用。例如,一个程序可能包含一个播放声音效果的简单例程。代替每次需要所述声音效果时编写和重写代码,程序员将特别触发相同的代码(即,例程)。
根据上下文和使用的编程语言,例程有时也被称为子程序、函数、过程、或方法。我们将在本书后面更详细地讨论术语。
美国信息交换标准码(ASCII)是一种字符编码标准,用于分配字母、数字和其他字符,以供计算和其他数字设备使用。本质上,你现在读的是ASCII码。作为一名程序员,你会经常碰到这个术语。“ASCII文件”通常被用作“人类可读文本文件”的简称该系统可追溯到1963年。
在今天的互联网上,最常用的字符编码标准是UTF-8,它包括所有的ASCII字母数字以及许多其他符号。
术语样板指的是或多或少自动插入到程序中的编程代码,几乎不需要编辑。当你在一个C++环境中开始一个新项目时,当代的开发工具通常会为你提供运行程序所需的样板代码。如果没有,您可以相对安全地将旧工作项目中的样板代码复制粘贴到新项目中,以便开始工作。
出于本书的目的,我们不会深究任何复杂的软件框架,但是理解这个概念是很重要的。
一个全栈是组成一个完整工作的web应用程序的软件,比如一个在线数据库。一个web应用程序通常分为两个区域:一个前端和一个后端。前端是为用户做的;它包含使用应用程序所需的所有用户界面元素。后端由web服务器、框架和数据库组成。因此,全栈开发人员是指那些了解在线应用程序编码前端和后端的人。
读完这一章,你有望对以下内容有所了解:
由Sun微系统公司创建并于1995年发布的Java很快成为一种被广泛采用的通用编程语言,尤其是在线环境(即云计算)。如今,用Java编写的软件驱动着无数的智能手机、数据中心和谷歌等无处不在的在线服务。截至2021年,对于新手和经验丰富的程序员来说,它是最受欢迎的编程语言之一。
虽然被称为JavaScript的编程语言与Java共享其前四个字母,但这两者几乎没有共同点。JavaScript由互联网先驱网景在90年代创造,用于当时新的浏览器技术。这种语言在互联网上仍然很活跃,为无数网站提供了额外的功能和可用性。
C#(读作C调)于2002年夏天向公众发布。该语言由微软开发,可用于创建从生产力软件到视频游戏的任何类型的现代应用程序。C#与包括Java在内的其他几种流行的编程语言共享一些特性。
你可能听说过C++,这是一种比C#更早也更复杂的语言。虽然C++通常能够产生更有效的输出,但用C#开发移动和web应用程序更简单。至于C++和C#的祖辈?向C(有时也称为过程C)问好,这是一种可以追溯到1972年的语言。
Python于1991年发布,它的名字来自于MontyPython,一个受欢迎的轻松娱乐电视节目。虽然Python的输出通常比用C#制作的软件慢,但Python在最近几年变得相当流行。它几乎可以处理任何事情,从制作简单的网络应用程序到较重的桌面软件。Python甚至在视频游戏行业找到了一个游戏原型的位置。
关于Python值得注意的是它对空白/缩进(通过按空格键和/或tab键创建的不可见字符)的敏感方式。我们将在本书的后面部分触及这种独特的方法。
现在,你可能已经听过关于二进制数字和计算机如何喜欢它的演讲。是的,在最基本的层面上,微处理器确实咀嚼1和0(它们真的挖掘字符串,比如01011010110)。输入中央处理器的这个最接近的级别被称为机器语言或机器代码的级别。在这个二进制级别之上有几个抽象层来帮助我们人类用计算机做我们的事情。
现在,在编程中,有高级语言和低级语言的区别。这与工具的质量无关。这种分类指的是一种编程语言与机器代码(即二进制代码)的接近程度。基本上,高级语言更接近人类语言,而低级语言对于外行人来说往往看起来相当晦涩。C#和Python被认为是高级语言,而C++则代表了所谓的中级语言,因为它为更高级的程序员提供了一些非常可靠的功能。
计算机不能马上执行任何编程语言。比如Java和Python都是所谓的解释型语言。这意味着清单中的每一行都被实时地一步一步地“解释”成机器代码。与其他类型的语言(编译语言)相比,解释过程在程序中表现为较慢的执行速度。
在能够运行之前,编译语言中的清单需要经历一个叫做编译的过程。这是指在程序执行之前,将程序的全部源代码翻译成机器语言。谢天谢地,编译过程是自动化的,所以你不需要任何程序员到机器代码的翻译手册。这是任何好的编码开发环境的功能。参见表2-1了解编译和解释编程语言之间的主要区别。
表2-1
编译语言和解释语言的主要区别
编译语言
解释语言
变量是一种临时存储的形式,程序的用户在使用软件的时候可以使用它。变量通常使用设备中的随机存取存储器(RAM)。这意味着,如果您关闭设备电源,存储在变量中的数据就会消失,除非先将其保存到硬盘等存储设备中。
变量可以用来存储游戏中玩家的名字,这只是一个基本的使用场景。从程序员的角度来看,变量在整个程序中被使用和重用。它们可以应用多种运算,从基本算术到复杂的数学公式。
每种编程语言中都有许多类型的变量。事实上,文本字符串、单个字母数字字符和不同范围的数值都有不同的变量类型。但是为什么要做这些区分呢?嗯,有时候你需要存储的只是一个字符,比如说字母“b”。在前面提到的场景中,为19位数值保留一种变量空间是对计算机内存的浪费,因此在大多数编程语言中有几种变量类型。
计算机不擅长猜测。大多数编程语言会明确区分字符串快乐变量和快乐变量。如果一个列表不起作用,可能只是在大写上有一些问题。
表2-2
Python中的六种主要数据类型
数据类型
描述
示例定义
||---|---|---||号|Python中的数字包括整数(整数)、浮点数和复数。可以在创建定义时进行计算|馅饼直径=21幸运数字=5+1圆环直径=6.26+0.11||字符串|字符串是字母数字字符的连续集合。单引号和双引号在它们的定义中都被接受|索德伯里的雷金纳德爵士昵称="注册"Color="紫色"||列表|列表由任何类型的值/变量组成。列表用方括号括起来,用单引号将字符串值括起来|jolly_list=[1,2,3,4,5]happy_list=['Hello',123,'Orange']||元组|与列表不同,元组是只读的,不能动态更新。元组用括号括起来|体面元组=(1,2,3)amazing_tuple=(1.12,“Ok”,456.5)||设置|集合是使用花括号初始化的无序值的集合。在集合中,重复的值会被丢弃|Fine_Animals={'猫','蝙蝠','蝙蝠','鸟'}三个伟大的数字={1,2,3,3,3}||字典|字典是无序的键值对,用花括号括起来|Friends={'name':'Yolanda','age':25}cars={'make':'Pinto','safety-level':'great'}|
您实际上不需要安装任何特定的软件来尝试Python、C#和Java编程的一些基础知识。这些语言有很好的在线编程实验环境。首先,现在是访问这些服务并体验Python变量的好时机。试试这些,坚持你最喜欢的:
准备好编译你的第一行Python代码了吗?启动一个在线编译器,将清单2-1输入编程空间。准备删除“helloworld”列表,默认情况下它可能在那里。当你完成输入后,点击编译器界面中的运行或执行来查看结果。
Fine_Animals={'Cat','Bat','Bat','Bird'}print("Myfavoritebeasts:",Fine_Animals)Listing2-1Noticehowtext(i.e.,“Myfavoritebeasts”)canbedisplayednexttoavariableusingacommainPython在清单2-1中,我们首先定义了一个变量,Fine_Animals,,这是一个适合我们目的的名字。然后我们继续使用print命令输出它的内容。这个输出应该是说我最喜欢的野兽:{'蝙蝠','猫','鸟'}(可能顺序不一)。第二只“蝙蝠”怎么了?它消失了,因为我们定义了一个Python集合数据结构(熟悉表2-2),其中不允许重复条目。
表2-3
Python中的一些显式类型转换函数
分配担任特定类型角色
函数调用
使用示例
||---|---|---||任何类型→整数|int()|phone_number="5551234"``new_variable=int(phone_number)``print(new_variable)||任何类型→浮点|float()|wholenumber=522``floatnumber=float(wholenumber)``print(floatnumber)||整数或浮点→字符串|str()|float_variable=float(2.15)``string_variable=str(float_variable)``print(string_variable)||字符串→列表|列表()|greeting="Hello"``a_list=list(greeting)``print(a_list)||字符串→集合|set()|fruit="Banana"``a_set=set(fruit)``print(a_set)|
有许多方法可以操作变量中的值。所有的基本算术(即加、减、乘、除、取幂和求根)都包含在任何编程语言中。下一个小清单用Python演示了这一点。清空您的编译器编程空间,如果您想查看它的运行情况,请键入清单2-2。
favorite_number=21print("Myfavoritenumberis",favorite_number)favorite_number+=2print("No,itwas",favorite_number)favorite_number-=1print("Wait..it'sactually",favorite_number)Listing2-2Thevariableusedisinbold在清单2-2中,我们使用加法和减法赋值操作符用于我们的算术目的(即,+=和-=)。下面的加法语句会产生相同的结果:favorite_number=favorite_number+2
有些场景要求程序将某些值解释为特定的变量类型。在用户需要输入各种数据类型的情况下,这可能是需要的。从一种数据类型转换到另一种数据类型的过程称为类型转换或类型转换。幸运的是,Python允许我们使用一些相当简单的方法来做到这一点。
当然,还有其他利用变量内容的方法。为了制作可用的软件,我们经常需要比较价值。为此有六个通用运算符(见表2-4)。这些操作符适用于几乎所有的编程语言,包括本书中提到的三种。
表2-4
Java、C#、Python和大多数其他语言中的主要比较运算符
操作员名
运算符符号
||---|---|---||等于|==|if(name=="Johnny")...||不等于|!=|如果(年龄!=21)...||不到|<|如果(年龄||超过|>|如果(能量>最大能量)...||小于或等于|<=|if(宠物蛇<=允许蛇数量)...||大于或等于|>=|如果(黄金>=1000)...|
现在让我们来看看清单2-3中的一些比较在Python中是如何工作的。
表2-5
变量类型
Java和C#
变量范围
||---|---|---||整数(整数)|int|从-2147483648到2147483647||性格;角色;字母|字符|单个字母数字字符(例如,B)||浮点数|浮动|6到9位小数之间的分数||双浮点数|double|最多17位小数的分数||布尔(逻辑运算符)|布尔值|真或假(即1或0)||文本字符串|字符串(Java),字符串(C#)|任意数量的字母数字字符|
不仅限于Python,Java和C#都有一些在线编译器。以下是一个选择,供您选择的乐趣。挑选一个你最喜欢的,这样你也可以用这些语言尝试一些实时编码。
此外,代码块可以限制变量的范围,即清单中特定变量有效的部分。两个不同的代码块可能无法访问彼此的变量空间。
Python不像Java或C#那样处理花括号。相反,该语言在表示代码块时使用空白(即空白字符或缩进)。在Python中,花括号是为定义字典或集合数据类型而保留的。
现在,在清单2-3中,我们用Python编写了一个程序,要求用户输入一个数字。然后程序在屏幕上显示一个基于这个输入的注释。让我们来看看Java是如何应对同样的考验的。这有点复杂,但不用担心。之后我们会分解它。
importjava.util.Scanner;publicclassHappyProgram{publicstaticvoidmain(Stringargs[]){Scannerinput_a=newScanner(System.in);System.out.print("Enteranumber:");intYourNumber=input_a.nextInt();if(YourNumber>10)System.out.println("Yournumberisgreaterthanten");if(YourNumber<=10)System.out.println("Yournumberistenorsmaller");}}Listing2-4AsimplelistinginJavademonstratinguserkeyboardinput与Python相比,Java确实需要更多的设置。用Java写的程序可以用所谓的Java包来扩展;这些基本上是数据容器,为你的项目添加新的特性和功能。清单2-4中的第一行为任何Java程序添加了交互功能,当我们需要用户输入时,它需要出现。
清单2-4只是将一个名为java.util的Java包合并到程序中。从这个包中,我们检索scanner函数,然后用它来读取键盘输入。在阅读本书的过程中,我们将浏览一些最常用的Java包。
Scannerinput_a=newScanner(System.in);这里发生的是我们创建了一个名为input_a.的扫描仪对象,我们可以将这个对象happy_object或pink_tutu。然而,最好坚持至少一个有点逻辑的命名方案。继续前进,我们会遇到下面几行代码:
System.out.print("Enteranumber:");intYourNumber=input_a.nextInt();在前面的代码片段中,我们使用Java的标准打印函数显示了一条消息。请注意它与Python的不同之处。接下来,名为YourNumber的整数变量(即整数)被初始化。然后,它被发送给一个名为nextInt()的函数,该函数等待用户输入,期望得到一个整数。前面提到的函数是我们在清单第一行导入的Java包java.util.Scanner的一部分。
你可能已经注意到分号(;)在清单2-4中。Java确实希望每个指令后面都有一个。此外,Java语法要求在变量比较和大多数函数中使用括号。所有这些惯例也适用于C#。
接下来,我们将浏览相同的清单;只是这次是用C#写的(见清单2-5)。您会发现它与Java中的有些相似,但是有一些关键的区别,我们将在后面讨论。
现在,与Java相比,C#对它的许多函数使用了不同的词汇。为了在屏幕上打印文本,我们有控制台。writeline。用户输入,我们有控制台。ReadLine如下行所示:
intYourNumber=Convert.ToInt16(Console.ReadLine());这里发生的是我们初始化一个整数变量,YourNumber,并把它传递给一个转换函数Convert。我们告诉它等待用户输入,并期待一个符号的16位整数值。这些是范围从-32,768到32,768的整数。这为我们的用户最有可能输入的内容提供了足够的空间。
无符号16位整数携带0到65,536之间的值,这意味着它们不能存储负值。如果我们需要存储非常大的数字,我们可以选择使用32位整数,,它们也有带符号的(-2,147,483,647到2,147,483,647)和无符号的(0到4,294,967,295)对应项。出于本书的目的,我们将坚持使用较小的数字。
您可能已经注意到单词class在我们的列表中出现了几次。这是面向对象编程(OOP)中的一个关键概念,一个流行的编程范例。书中提到的三种语言都在不同程度上融入了OOP。
任何真实世界或抽象场景都可以用OOP优雅地表达出来。这种方法中的两个基本构件被称为类和对象。简单来说,类定义了对象的蓝图。例如,你可能正在开发关于园艺的软件,并编写一个名为Plant的类。然后,您可以使用Plant类来调用称为对象的单个实例来填充您的虚拟花园。你以这种方式创建的各种不同的个体植物群将彼此共享特征和变量,正如它们的起源类中所定义的那样。以后更改工厂类的零件会影响属于该类的所有未来对象。您还可以创建Plant的子类,以满足您计划建模的各种玫瑰和郁金香(以及花园侏儒类)。
OOP为软件开发者提供了许多好处。其中包括代码的可重用性和通过类封装的开发安全性。面向对象编程有许多迷人的方面。我们将在本书的后面更深入地探讨这个范例。
除了变量之外,我们有一堆逻辑结构来处理我们的编码工作。这些结构构成了所谓的流量控制。当任何节目单被输入时,计算机会从上到下阅读它。通常,这个程序中的处理要重复许多次。因此,具备循环和条件程序流的能力是有意义的。
编程中的循环可能会向您介绍迭代的概念。迭代是将特定的步骤重复预定的次数,以获得想要的结果的过程。编程环境中重复的动作序列被称为循环。有许多方法可以创建这些循环,这也取决于所使用的语言。这些结构的示例实现见表2-6。
表2-6
三种语言中的流控制示例
Java中的“While-loop”
C#中的“For循环”
Python中的“For-loop”
||---|---|---||//让我们初始化一个变量intI=3;而(i>0){System.out.println("三个hello");-我;}|//这是一个迷人的循环for(intI=0;我<3;i++){控制台。WriteLine(“你好!”);}|#这是一个有趣的循环对于范围(10)内的i:打印(“你好号码”,I)|
表2-6中的第一个迭代方法是用Java演示的while循环。这种方法执行操作,直到满足while函数中的条件。在我们的例子中,当变量i大于零时循环运行。除了在屏幕上打印文本时命令语法的不同,表2-6中的while循环在Java和C#中都是一样的。
表2-6中C#的例子可能看起来有些复杂。正在讨论的结构,for-loop,是一种古老的技术,由现在将要讨论的几个元素组成。
头部(即从的开始的部分)包含关于主体(即由花括号包围的部分)将被执行多少次的指令。在我们的示例中,头部分如下所示:
同样,除了命令语法上的差异(即System.out.println与Console。WriteLine),表2-6中间的例子在Java和C#中都是一样的。
您可能会注意到在我们的Python循环中明显缺少分号和花括号。包含print命令的行确实包含在循环的代码块中,只使用缩进。此外,Python中的for-loops可以简单地使用一个叫做range的漂亮函数来设置迭代次数,而不是Java和C#中稍微复杂一些的结构。
有时我们需要记录代码的变更。虽然纸和笔都可以,但是在清单中做这个更好。要添加计算机无法解析的行,我们可以在清单中加入特殊字符。对于Java和C#,我们使用两个正斜杠,对于Python,我们使用散列标记,如清单2-5所示。
读完这一章,你将有望理解以下内容:
在第三章中,我们将超越在线编译器,为您提供一些优秀的可下载软件,并加深您对本章概念的理解。当谈到软件开发环境时,我们将涉及Windows、macOS和Linux。
本章致力于向您介绍集成开发环境的乐趣。虽然在线编程环境对您的前几份清单来说是不错的,但是在您自己的计算机上安装一些专用的编码软件会让您受益匪浅。幸运的是,有很多免费的ide可供你下载。我们将涵盖2021年三个最流行的操作系统的各种软件。但首先,我们将解决另一个基本概念:计算架构。
一个时钟周期代表一个CPU内执行的单个动作。在每个时钟周期中,CPU执行基本的内部操作,这些操作构成了计算机生态系统中任何更大任务的一部分。现在,时钟速度指的是一个CPU每秒可以召集的时钟周期数,通常用千兆赫(Ghz)来表示。例如,一个2.1Ghz的CPU每秒提供21亿个时钟周期。CPU每秒提供的时钟周期越多,系统处理数据的速度就越快。
现在,术语计算架构被用来描述一个CPU每个时钟周期可以处理多少数据。目前市场上基本上有两种主要的计算架构:32位和64位。后者正迅速成为大多数计算类型的事实架构。为64位架构编写的软件可以更好地利用系统资源,如RAM,同时通常比32位架构执行得更快。
架构实现发生在硬件和软件中。只有64位CPU可以运行64位操作系统,同时仍然提供与旧的32位软件(和操作系统)的兼容性。然而,64位操作系统通常甚至不能安装在具有32位CPU的计算机上。
到21世纪初,支持64位计算的CPU变得越来越流行。除非你使用的是真正的老式电脑,否则你很可能已经准备好了64位计算,至少在硬件方面是如此。有关这两种架构的概要,请参见表3-1。
大企业正在彻底放弃32位技术。事实上,从macOSCatalina(10.15)开始,苹果完全放弃了对32位软件的支持。微软也正在将32位世界抛在身后。截至2020年5月,任何现成的电脑都将只配备64位版本的Windows10。在三大操作系统中,只有一些Linux发行版仍然广泛适用于32位硬件。
表3-1
32位和64位计算架构的比较
32位架构
64位架构
||---|---|---||特征|仅运行32位版本的操作系统,可以运行大多数传统的16位软件|运行32位和64位操作系统,通常不支持16位软件||每个系统的理论最大RAM|4千兆字节或4,294,967,296字节|171亿GB或18,446,744,073,709,551,616字节||系统中的典型RAM数量|1到4GB之间|介于8和32GB之间|
出于本书的目的,您不需要运行任何64位软件;32位操作系统就可以了。但是,为了让您的设备经得起未来的考验,您应该考虑尽快迁移到64位操作系统。
就像你现在可能知道的,计算的最小单位是比特。和其他量一样,仅仅用原子单位来衡量事物是不切实际的。因此,我们有字节、兆字节和千兆字节,仅举三个例子(见表3-2)。
公制单位(即使用10的幂乘数的单位)在个人计算的早期工作得很好。然而,使用这些乘数并不完全准确。例如,按照公制,1千字节的文件实际上是1024字节,而不是1000(103)。
典型的硬盘驱动器的额定容量为250GB(即250,000,000,000字节),实际容量为232.8GB。硬件厂商一般不会提这些东西。我想你可以走了。
1998年,国际电工委员会(IEC)创建了更精确的测量方案。新系统使用2的幂乘数。例如,一千字节变成了1024字节大小的千比字节(210=1024)。这些新单元非常需要,因为它们在测量更大的数据池时更加精确。
表3-2
最常用的公制和IEC数据存储单元的比较
米制单位
价值(公制)
IEC单位
价值(国际电工委员会)
||---|---|---|---||位(b)|0或1(原子/最小单位)||||字节(b)|八位||||千字节(kB)|1000字节|基布利特(基布利特)|1024字节||兆字节|100万字节|Mebibyte(MiB)|1,048,576字节(1024个字节)||千兆字节|10亿字节(十亿字节)|吉布比特(gib)|1,073,741,824字节(1024兆字节)||兆兆字节(TB)|10亿字节(一万亿字节)|提比布|1099511627776字节(1024)|
您可能需要检查操作系统的架构是32位还是64位。又快又简单。
好的开发环境提供搜索特性、语法突出显示,在某些情况下还支持多种编程语言。我们将首先为您设置一个专门为Java开发打造的环境,由EclipseFoundation开发的强大的EclipseIDE(见图3-1)。
Eclipse项目最初是由IBM在2001年11月创建的。EclipseFoundation于2004年作为一个独立的非盈利组织出现。它是作为一个围绕Eclipse的开放透明的社区而创建的。
Eclipse适用于Windows、macOS和Linux。访问下面提供的下载页面,只需点击反映您正在运行的操作系统类型的链接。然而,有一个警告。Eclipse的最新版本只适用于现代操作系统的64位版本。如果您仍然在使用32位操作系统,请查看Eclipse旧版本的专用链接。它们也很适合我们的目的。
图3-1
行动中的Eclipse
Linux存储库可能托管一个过时版本的Eclipse。为了在Linux中安装最新版本,我们将使用CanonicalLtd.提供的Snapcraft方法。这种方法应该适用于Linux的所有主要发行版。打开终端窗口,输入以下行:
sudoaptinstalldefault-jreFedoraLinuxusersmightneedtoinputthefollowingTerminal-commands:sudodnfinstalljava-latest-openjdk.x86_64sudodnfinstalljava-latest-openjdk-devel.x86_64sudosnapinstall--classiceclipse在安装过程中,系统可能会提示您输入密码。成功安装后,您应该会看到以下消息:安装了来自Snapcrafters的eclipse(版本信息)。
启动Eclipse时,会提示您为Eclipse工作区选择一个目录。默认设置适用于大多数用途。让我们创建一个新项目。这是通过从顶部菜单栏导航到文件新建Java项目来完成的。接下来,您应该会看到一个等待您的项目图块的窗口。输入一个并点击创建。
您将被带到Eclipse中的主项目视图。我们手头上还没有一个实用的Java应用程序。现在,在左边,你会看到项目浏览器。左键单击您的项目名称。从顶部菜单中选择档新级。输入这个新类的名称;它不必共享您的项目名称。
接下来,确保你已经勾选了publicstaticvoidmain(Stringargs)旁边的复选框。这给了你的项目一个Javamain方法。没有它,你无法执行你的项目。最后,点击完成进入你全新的Java代码清单。您现在可以在main方法下开始编码了。
现在,让我们回顾一下您的C#需求的一些选择。我们将从微软的VisualStudio开始,这是一个流行的多平台IDE,充满了伟大的特性,包括动态错误下划线。该软件适用于Windows和macOS。
让我们从Windows和macOS的VisualStudio安装开始。首先,您需要下载完全免费的社区版的正确安装程序。这是一个用于多种语言的健壮的IDE,包括C#。
启动VisualStudio安装程序后,您最终会看到一个关于工作负载的屏幕。这些基本上是指VisualStudio中提供的各种语言的不同实现场景。您将看到C#和其他语言的四类工作负载;分别是Web&云、桌面&移动、游戏、和其他工具集(见图3-2)。对于我们的编码需求,让我们勾选旁边的复选框。NET桌面开发。这将很好地为我们设置C#语言。
最后点击窗口右下角的安装。您也可以选择在下载时安装,或者先下载所有必需的文件,然后再开始安装过程。
图3-2
MicrosoftVisualStudioCommunityEdition的安装屏幕
第。NET是微软在2002年创建的一个软件框架,在专有许可下发布。它为C#、C++和其他流行语言提供了简化的开发。该框架在2016年以的名义接受了一次大修。NET核心。这一次,它以开源和跨平台的形式出现,包括对macOS(以及在一定程度上对Linux)的支持。此后,微软不再将新版本的名字改回.NET。
几分钟后,您将到达VisualStudio主窗口。当你点击创建新项目时,你会看到各种各样的编程模板,包括那些专门针对C#的模板。对于我们的需求来说,控制台App是合适的。过一会儿,VisualStudio将创建您的项目文件,您可以开始在main函数下编码。
在所谓的控制台应用程序和那些具有图形用户界面(GUI)的应用程序之间有一个普遍的区别。前者使用基于文本的基本界面,而后者为用户提供更多的视觉效果,通常还提供额外的输入方法(如鼠标或触控板)。控制台应用程序有时会利用基于文本的伪图形来模拟几何形状。
尽管外观古怪,但控制台应用程序为开发人员提供了很高的效率;毕竟,(音频)视觉所消耗的所有资源都可以用于程序的核心功能。
您可以通过在控制台应用程序领域中工作来学习所有基本的编程技巧。尽管我们将触及基于GUI的开发,但本书的重点主要是基于文本的环境。
截至Q12021年,VisualStudio不可用于Linux。然而,MonoDevelop提供了在灵活的IDE中开始编写C#所需的一切。
从前面的下载页面(例如Debian)导航到最接近您当前使用的Linux发行版和版本的Mono存储库。接下来,您将复制粘贴许多命令到您的终端窗口。根据您的硬件,您可能需要等待几分钟才能完成安装。之后,MonoDevelop应该安全地驻留在您的应用程序文件夹中。
点击MonoDevelop图标打开MonoDevelop。导航到文件新解决方案。一个新的窗口将会打开。点击。NET下其他,选择控制台项目。最后点击右下角下一个的。将打开另一个窗口,提示您输入项目名称。输入您认为合适的内容,然后点击创建。**
接下来,您应该会看到一个用C#编写的通用helloworld应用程序的清单。或者导航到运行启动而不调试,或者单击MonoDevelopIDE左上角的播放图标来执行列表。
在MonoDevelop中尝试运行/调试程序后,如果出现以“找不到指定文件”结尾的错误提示,您可能需要在终端窗口中执行以下附加行:
cd/usr/lib
sudomkdirgnome-terminal
cdgnome-terminal
sudoln-s/usr/libexec/gnome-terminal-server
现在,您已经准备好为Linux开发一些漂亮的控制台应用程序了。
当使用Python进行开发时,有一个软件是最优秀的。PyCharm是一个多平台IDE,具有智能代码完成等优秀特性。该套件有付费版和免费版;我们将与后者合作。以下链接将引导您找到您的操作系统支持的PyCharm版本。
PyCharm在Windows和macOS上的安装过程非常简单。对于前者,运行您下载的可执行文件,并按照屏幕上的指示进行操作。对于macOS,只需双击图像文件(以结尾。dmg),并将PyCharm图标拖到应用程序文件夹中。
在Windows上安装PyCharm时,即使您的帐户没有密码保护,也可能会出现密码提示。只需单击“取消”继续安装。
要开始试用PyCharm,请点击创建项目。然后,您将有机会命名您的项目,并为其选择一些其他选项。项目名称同时也是所有PyCharm项目文件的目录位置。当你选定了一个合适的标题后,点击窗口右下角的创建。
为了使开始更容易,PyCharm为您提供了创建欢迎脚本的选项。最好启用这个选项。创建新项目时,确保勾选了Createamain.pywelcomescript旁边的复选框。
第一次打开新项目时,文件main.py将填充一些Python代码。你可以删除所有的代码,然后在它的位置键入你自己的代码。
让PyCharm在您的Linux上运行的最佳方式是使用snap包。只需打开一个终端窗口,输入下面一行:
sudosnapinstallpycharm-community--classic软件害虫:臭虫你是否曾经因为电脑堵塞和/或文本文件丢失而结束了你的工作?你可能会在运行中遇到一个软件错误。软件环境中的bug意味着由错误的程序代码引起的小故障;它们可能是程序员的打字错误,或者更常见的是一些神秘的设计错误。所有这些操作系统的软件更新基本上都是为了修复错误(有时还会提供新功能)。参见表3-3了解最常见的软件错误和问题。
表3-3
最常见的软件问题/错误类别
软件缺陷
例子
软件问题
||---|---|---|---||句法的|不正确地使用语言语法,例如,使用错误的变量比较运算符|连接|缺少用户界面功能,例如,缺少导航按钮或其他关键元素||算术|被零除,可变范围溢出|安全|薄弱的身份验证,关键系统组件的不必要暴露||逻辑学的|较差的程序流控制,例如无限循环或折衷循环|沟通|缺少用户文档,用户界面元素的标签不好,不直观的错误提示||资源|使用未初始化的变量,通过不释放不需要的变量空间来耗尽系统RAM|团队合作疏忽|代码元素/变量的标签不合逻辑,注释不当|
调试是发现并修复软件中发现的bug的艺术。一个调试器为软件开发人员提供工具,在错误发生时,在它们可能对用户计算机造成严重破坏之前修复它们。这是solidIDE的另一个特性,自然属于本章介绍的每个解决方案。
调试范围从IDE编辑器中简单的错误突出显示到详尽的数据收集和分析。典型的调试组件允许程序员在软件运行时检查正在开发的软件,如有必要,可以一行一行地检查。代码编写的高级编程语言,如Java和Python,通常更容易调试。低级语言,如过程C,留给程序员更多的控制权;这可能会更频繁地导致内存损坏等问题。
繁重的调试对于较大的软件项目来说是绝对必要的。出于我们的需要,这个话题我们不必深究。在这一点上,你知道这个术语的意思就足够了。
调试与软件开发中有时被忽视的领域有关:测试。虽然测试包括调试,但还有更多。测试团队负责报告软件产品在其预期的使用场景下是否正常工作。然而,即使是大规模的测试过程也不能找到软件项目中的每一个缺陷。
测试过程的细节取决于目标受众的类型。视频游戏开发人员,尤其是对于较小的团队,有时会在测试上偷懒(这对他们非常不利)。银行和金融领域的基本软件预计将接受最严格的测试。较大的软件项目需要一个高度组织化的测试团队。由于经济原因,大型软件的测试通常外包给国外的企业。
现在,我们将详细介绍一些最常见的代码调试方法:
读完这一章,你会对以下内容有所了解:
下一章将详细介绍面向对象编程(OOP)的奇迹。正如在第二章中提到的,这是一个非常重要的范例,每个初学的程序员都应该能够使用。
正如在第二章中提到的,目前基本上有两种主要的编程范例:过程化和面向对象。20世纪70年代末是计算机科学令人兴奋的时期。在此之前,大多数编程都是严格在过程语言领域中完成的,这种语言是在所谓的“自顶向下”的设计范式下运行的。基本上,使用这种方法,程序员最后处理细节。大多数焦点都集中在程序的主要功能上。
C++的发布拉开了面向对象革命的序幕。该语言于1985年发布,很快被广泛应用于大多数用途。使用“自下而上”的设计方法,C++和其他面向对象的语言专注于首先定义数据,这通常是根据现实生活中的现象建模的。此外,与它们的程序性对应物相比,它们提供了大量的附加功能。这包括封装,这是一种通过实现访问说明符将代码部分相互隔离的技术。接下来我们将更详细地研究封装和其他面向对象的细节。
参见表4-1了解过程式编程语言和面向对象编程语言的主要区别。
表4-1
两种主要编程范例之间的主要区别
过程程序设计
面向对象编程
||---|---|---||示例语言|(程序性)C,Pascal,BASIC|C#、C++、Java、Python||基于|抽象世界|真实世界的场景||方法|自上而下:主要问题被分解成子过程|自下而上:首先创建数据类||强调|功能(即程序)|数据(即类)||遗产|不支持|支持||封装(数据安全)|低:没有访问修饰符|高:多级访问修饰符||方法重载(多个方法同名)|不支持|支持|
让我们重温一下面向对象编程(OOP)的最基本的概念,因为我们在第二章中只触及了其中的一些。我们将继续讨论类和对象的概念。到现在为止,你可能知道在OOP中,类是一种创建代码对象的蓝图。
现在,尤其是对于较大的软件项目,可以说,在编写一行代码之前,将笔放在纸上通常是一个好主意。可视化面向对象软件的最好和最流行的工具之一是使用通用建模语言(UML)。由RationalSoftware在20世纪90年代中期创建,UML已经成为软件工程中无处不在的工具。让我们用一个关于冰淇淋的类图来取样一些(见图4-1)。
图4-1
一个简单的类图展示了UML的基础知识
图4-1(即冰淇淋)中最上面的盒子是一个抽象类。基本上,这些类不能用来创建对象。那为什么有他们?嗯,抽象类可以保存和普通类相同类型的信息。它们也可以有子类。抽象类的目的是为它的子类提供一些共享信息来继承和创建对象。在许多情况下,它们的使用简化了设计过程。
抽象类下面的三个盒子被称为子类。他们展示了继承,这是OOP中的一个关键概念。香草、巧克力和鬼椒接收它们的超类冰激凌内置的所有变量和方法。注意这些盒子是如何连接的。不同种类的箭头在UML中描述不同的事物。如图4-1所示,一个空的箭头意味着简单的继承。在UML中,人们还必须注意箭头的方向。
现在,你在UML中用斜体表示抽象类的名字;常规课程通常不会以任何方式风格化。接下来,一个类框列出了它的变量和它们的数据类型。变量前的加号(+)表示它是公共的,而减号(-)表示私有变量。最后,我们列出在我们的类中找到的任何方法,就像我们对图4-1中的Display_Flavor()和所有其他方法所做的那样。
除了我们到目前为止介绍的元素之外,UML还有更多内容。我们将在本书后面的章节中更详细地介绍这种不可思议的建模语言。现在,只要你理解UML如何帮助你可视化一个软件项目就足够了。
Get和Set一般来说是Java和OOP中非常重要的关键词。与return命令一起,它们用于检索和定义类中的数据。提醒一下,函数(也叫方法)是一段只有被调用时才运行的代码。
publicclassGeezer{//Theprivate-accessmodifierisusedtolimitaccessfromotherclassesprivateStringname;//TheGet-functionisusedtoretrievenamevariable//Itsnamingconventionisnon-capitalized"get"andcapitalizedvariablepublicStringgetName(){returnname;}//TheSet-functiondefinesdatapublicvoidsetName(StringnewName){this.name=newName;}}Listing4-1AclassdefinitioninJavawithGet-andSet-functions(filenameGeezer.java)清单4-1不能在ide或在线开发环境中执行,因为它缺少Javamain方法。该列表仅用于演示目的。我们将很快进入实际可执行的Java代码。
我们通过给它一个名字来开始我们的类定义,这个名字也是它的文件名。在我们的例子中,应该是Geezer。我们做的第一件事是定义一个类型为字符串的变量(显然是为了存储老人的名字)。变量定义前的关键字private被称为访问修饰符。私有方法和变量只能在定义它们的类中访问(在我们的例子中,只能从Geezer中访问)。
getName方法使用return关键字来检索一个怪老头的名字。该方法被创建为公共的和值字符串。这只是因为它应该返回一个字符串变量的值。
接下来让我们分解我们在清单4-1中定义的setName方法。关键字对publicvoid指定了一个不返回值的方法。它通常适用于集合函数,因为它们用于定义变量,而不是从变量中检索值。Java中的关键字this简单地寻址包含该方法的对象。
类是组织数据的好工具,但是它们能做的更多。使用geezer类作为起点,让我们添加代码来用Java创建一个实际的对象(参见清单4-2)。
publicclassGeezer{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(StringnewName){this.name=newName;}//Weaddamain-methodsowecanactuallyexperimentwithourclasspublicstaticvoidmain(Stringargs[]){//Nextwecreateanobjectusingthegeezer-classasablueprint//We'llcallourobject"some_geezer"//anduseJava'sbuilt-innew-methodtobringittolifeGeezersome_geezer=newGeezer();//NextweinvokethesetName-methodtogiveourgeezeranamesome_geezer.setName("John");//Finallyweaccessourgeezer-objecttoprintoutaname//intwodifferentwaysSystem.out.println("Hello,mynameis"+some_geezer.name+"thegeezer!");System.out.println("Canyourepeatthat"+"It's"+some_geezer.getName());}}Listing4-2AclassdefinitionwithamainmethodinJava(filenameGeezer.java)在清单4-2中,我们使用了一个所谓的点操作符(即some_geezer.name)和getName方法从我们的对象中读取数据。点运算符也称为成员运算符。
在继续OOP之前,让我们先看看Java开发中最重要的三个软件组件。毫无疑问,在您的编程冒险中,您会经常遇到这些术语。首先,有Java开发包(JDK)。这是用Java编码所需的类和工具的核心集合。JDK有几个品种。
接下来,我们有了Java运行时环境(JRE)。该组件用于将JDK内部完成的代码输出与一些额外需要的软件库相结合,从而允许Java程序实际执行。
最后,我们有Java虚拟机。在桌面PC上创建的Java程序可以在任何安装了JVM的设备上运行。因此,这种在专用虚拟机上运行Java的方法创建了很大程度的平台独立性。
你可能还记得第二章的内容,Java是一种解释语言。当执行用Java编写的程序时,编译器生成字节码。这是一种中间格式,需要Java虚拟机(JVM)来运行;没有它就不能启动字节码程序。由于JVM可用于大多数现代计算机和设备,这种方法使得Java几乎普遍独立于平台。
已经用Java创建了我们的(可能的)第一个对象,现在让我们用C#做同样的事情。参见清单4-3,其功能与清单4-2完全相同。这将展示Java和C#在语法上的许多相似之处。
usingSystem;publicclassGeezer{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(StringnewName){this.name=newName;}publicstaticvoidMain(string[]args){Geezersome_geezer=newGeezer();some_geezer.setName("John");//ThenexttwolinesdifferthemostbetweenourJavaandC#listingsConsole.WriteLine("Hello,mynameis"+some_geezer.name+"thegeezer!");Console.WriteLine("Canyourepeatthat"+"It's"+some_geezer.getName());}}Listing4-3AclassdefinitionwithamainmethodinC#(filenameGeezer.cs)Java和OOP中的静态和公共方法我们现在将更深入地研究编写各种方法。OOP中基本上有两种方法:静态和公共。我们已经在清单4-2和4-3中试验了后者。
Note
一个方法实际上可以使用两个限定符,著名的Javamain方法publicstaticvoidmain()就是这样。
现在,这两个变种之间的主要区别是静态方法不需要使用对象来调用;您可以在没有任何特定类实例的情况下调用它们。然而,静态方法不能使用类变量,如清单4-4所示。
publicclassHappyMethods{privateintx=10,y=10;//Astaticmethodcan'tuseourclassvariablesstaticvoidmyStaticMethod(){System.out.println("Hello!I'mastaticmethodandIcan'tusexory.");//System.out.println(x+"+"+y+"="+(x+y));//Thelineabovewouldreturnanerror}//ApublicmethodcanuseourclassvariablesforsomerudimentaryarithmeticpublicvoidmyPublicMethod(){System.out.println(x+"+"+y+"="+(x+y));}//Ourmainmethodpublicstaticvoidmain(String[]args){myStaticMethod();//CallthestaticmethodHappyMethodsmyObj=newHappyMethods();//CreateanobjectofHappyMethodsmyObj.myPublicMethod();//Calltheobject'spublicmethod}}Listing4-4AclassdefinitionwithamainmethodinJava(HappyMethods.java)构造函数方法对象从类定义中接收所有变量的初始值。但是,无论何时创建对象,都可以调用构造函数来设置全部或部分变量数据。
您可以通过定义接受额外属性的方法来创建新的构造函数。这些属性然后根据需要传递给每个对象,以替换类中定义的原始值。参见清单4-5中的示例。
构造函数方法必须与包含它的类同名。在我们的示例程序中,这两个构造函数都被命名为Movie,根据它们的原始类。
第一个对象,处理理论上(但确实很精彩)的电影杰克和吉尔2,是使用接受单个字符串的构造函数方法创建的。清单4-5中的第二个对象使用了更通用的构造函数,它接受一个字符串和一个整数。我们示例中的第三个也是最后一个对象是使用最基本的构造函数创建的,它不接受任何属性;它会将默认值(“Jack和Jill”和2011)分配到我们的对象中,就像输入到类定义中一样。
在OOP中,你可以拥有多个同名的方法,只要它们的参数的数量和数据类型不同(参见清单4-6)。
最后,我们将三个字符串(包括中间的首字母)组合成一个受欢迎的英国流行歌手的名字。
到目前为止,您已经在我们的清单中多次遇到了访问修饰符。接下来让我们回顾一下。毕竟,它们在OOP中非常重要。此外,对他们来说,除了私人的、公共的,还有更多。参见表4-2和4-3分别了解Java和C#中最常见的访问修饰符。您会注意到两种语言有不同数量的访问修饰符,尽管它们基本上都遵循相同的OOP逻辑。此外,类可以访问Java中的包。
表4-2
Java中最常见的访问修饰符
访问修饰符
当与一起使用时
易接近
此外,从程序员的角度来看,正确使用访问修饰符使程序更具可读性。更新和维护封装的软件项目通常比那些过程化的(即非面向对象的)项目更直接。
记住,OOP中的封装包含两层意思。首先,它是一个用来描述使用类将数据与方法配对的方法的术语。其次,它指的是使用访问修饰符在编程级别限制对数据的直接访问。
*现在,让我们回顾一下六个C#访问修饰符。它们现在看起来很容易互换,但是当你读完这本书的时候,你会对它们都很熟悉,并且知道它们是如何被需要的(见表4-3)。
表4-3
C#的六个访问修饰符
让我们回到编码上来。到目前为止,本章的大部分内容都使用了Java。接下来,让我们为C#创建一个原始的OOP清单来演示它对访问修饰符的处理(参见清单4-7)。
接下来,我们将继续讨论列出4-7的主要方法。这里,我们创建了两个对象,分别来自我们之前定义的每个类。请注意,用C#创建对象的语法与Java使用的语法完全相同。
我们的main方法中的最后一行显示了一条包含字符串变量内容的消息;这就是所谓的格式的字符串。在C#中,变量用花括号显示在文本中(C派生语言一般真的很爱用)。元素{0}引用第一个(在本例中是唯一的)变量,我们将在消息旁边显示它。如果我们有第二个变量要在字符串中打印出来,我们会用{1}—等等。
为了让你对统一建模语言的奇迹有进一步的准备,让我们把清单4-7变成UML,好吗?实际上,这种语言不仅仅是类图;使用UML,我们还可以可视化对象。在图4-2中,我们有清单4-7的类图(左)和对象图。
图4-2
UML中清单4-7的类图(左)和对象图
尽管Java和C#在语法和逻辑方法上非常接近,但还是有一些细微的差别,一开始可能会让人感到困惑。例如,两种语言对受保护的访问修饰符的处理是不同的。
我们没有忘记Python,是的,它是最面向对象的。尽管这种语言支持所有主要的面向对象技术,但语法与Java和C#有很大不同。首先,大多数可爱的大括号仍然不存在。另外,Python中的构造函数是用关键字init()定义的(每边有两个下划线)。
在Python中,空白成为一个非常重要的因素。正如第二章提到的,缩进是Python语法不可或缺的一部分,用于表示清单中的代码块。
现在,让我们用Python制作一个简单的类应用程序。在清单4-8中,我们创建了一个类和一个构造函数方法,并使用该类实例化了一个对象。
classPublisher:def__init__(self,name):self.name=name"Createnewobject,cool_publisher,fromtheaboveclass"cool_publisher=Publisher("Apress")"Displayitsname"print(cool_publisher.name,"iscool!")Listing4-8AsimplePythonlistingdemonstratingOOP仔细阅读清单4-8,您会立即注意到Java和C#的区别。首先,类Publisher由三个独立的代码块组成,由三个不同级别的缩进所分隔。如果不遵循这种格式逻辑,Python实际上会抛出一个错误。幸运的是,大多数PythonIDEs会在适当的地方自动添加空白。
接下来让我们用Python尝试一些稍微复杂一点的东西(参见清单4-9)。
我的品种是Leminkirjava,我的直径是3英寸
我在芬兰长大
我的品种是法国鱼种,我的直径是5英寸
我在法国长大
列出的马铃薯品种总数:2
我们稍微复杂一点的Python清单引入了几个新概念。其中之一是全局变量。这些是指可以在Python列表中的任何地方使用的变量,包括方法内部和外部(任何类)。
接下来在清单4-9中,我们有一行definit(self,args),它是我们的Potato类的唯一构造函数。它不接受特定的数据类型,而是接受一个参数列表,如表达式*args所示。
Python本身不支持方法重载,不像Java和C#。如果我们为了重载的目的在一个类中输入任意数量的方法,这些方法中的每一个都会简单地覆盖前一个。
为了给变量赋值,我们使用args[0]作为第一个参数,使用args[1]作为第二个参数。可以看出,Python从零开始计算参数。
现在,Python有了一个方便的内置函数来确定列表和其他数据结构的长度,len。这在我们的清单iflen(args)>2:中的下一行中使用,它的意思是“如果参数长度超过两个”基本上我们的程序最多接受三个参数;剩下的就干脆丢弃了。当我们给对象potato2总共四个参数时,这反过来被证明;最后一个论点没有效果。
至于我们的全局变量potato_count,每当从potato类实例化一个新对象时,它的值就增加1。这自然相当准确地反映了土豆对象的总数。
在结束这一章之前,让我们再讨论一个重要的话题。Python中的继承实现起来非常简单(参见清单4-10)。
自然,Python中的继承不仅仅适用于变量;方法也会被传递。在清单4-10中,printType是一个源自计算机类的方法。但是,我们可以使用从Desktop类实例化的对象来调用它。
在我们郑重地结束这一章之前,让我们再来看一下Python(参见清单4-11)。
classToiletPaper:pass"CreatetwoobjectsfromclassToiletPaper"type1=ToiletPaper()type2=ToiletPaper()"Addcostandbrand-variablesintotheclass"type1.cost=4type1.brand="Supersoft"type2.cost=2type2.brand="Sandpaper"print("Wesell",type1.brand,"and",type2.brand)print("Theirpricesare",type1.cost,"and",type2.cost,"dollars,respectively")Listing4-11APythonlistingdemonstratingadhocclassmodification在清单4-11中,我们使用关键字pass来创建一个没有变量或方法的类。在Python中,我们甚至可以实例化这些空白类,这就是接下来要做的。现在是适度有趣的部分,向临时对象修改问好。在Python中,通过引用空白类实例中不存在的变量,可以为所述实例创建新的数据结构。我们称之为属性绑定。
属性绑定也适用于类(参见清单4-12)。
classToiletPaper:pass"Createobjectandaddcostandbrand-variablesintoit"type1=ToiletPaper()type1.cost=400type1.brand="Sandpaper""Addcostandbrandintotheclass"ToiletPaper.cost=4ToiletPaper.brand="Supersoft""CreateanobjectfrommodifiedclassToiletPaper"type2=ToiletPaper()print("Wesell",type1.brand,"for",type1.cost,"dollars")print("Wealsosell",type2.brand,"for",type2.cost,"dollars")Listing4-12APythonlistingdemonstratingattributebindingforclasses清单4-12再次从我们定义一个空类开始。然而,这一次我们将属性绑定到这个类,而不仅仅是它的对象。从清单的输出可以明显看出,向类中添加任何数据都不会覆盖对象以前的绑定。
读完这一章,你将有望学到以下内容:
这就结束了第四章,这是本书相当密集的部分。在下一章,我们将深入探讨一些高级的Java主题,比如文件操作和多线程。*
到目前为止,您可能已经相当熟悉了强大的面向对象语言Java中的基本编程元素。现在是时候转向更高级的概念了。在本章的后面,我们将从多线程和Java中的基本错误处理开始。继续,我们最终会谈到文件操作这个有趣的话题。
图5-1
演示双核CPU内部多线程的简单图表
一般来说,系统拥有的内核越多,处理数字的效率就越高。这取决于您的操作系统以及它是否支持多核计算;几乎所有现代操作系统都具备这样做的条件。然而,并非所有第三方软件都是为了充分利用多核处理而编写的。在这种情况下,软件只使用系统中的一个内核。
现在,并发运行线程的数量通常不等于CPU可支配的内核数量。记住,一个软件应用程序可以为不同的任务调用多个线程。这意味着单核系统可能会有几个线程同时运行。多核CPU提供的是在众多内核之间共享所有这些线程的工作负载的可能性。在具有六个或更多CPU内核的系统上,这可以极大地提高计算速度。
你可能偶尔会遇到术语超线程。这是指英特尔的专有技术,理论上,它可以提供两倍于处理器实际配备的CPU内核。超线程技术于2002年推出,它与操作系统协同工作,以增加可用内核的数量。英特尔声称,与没有超线程技术的相同CPU相比,配备超线程技术的CPU可以全面提升性能。然而,在现实生活中,超线程技术的优势在很大程度上取决于应用。这项技术确实在视频编辑和3D建模等CPU密集型场景下蓬勃发展。
线程可以分为不同的状态,这些状态构成了一个线程生命周期。接下来,让我们详细了解这五个阶段:
让我们看一个用Java演示多线程的实际程序(见清单5-1)。
虽然清单5-1的输出是以有序的方式呈现的,但是其中的三个线程是按照多线程的方法并发运行的(参见图5-2)。
此外,在连续几次运行清单时,您可能会发现不同的线程以不同的顺序显示它们的输出。对于一个不同步的程序来说,这是非常正常的。
图5-2
清单5-1中程序流程的简化图
CPU提供的处理能力是一种有限的资源。因此,多个执行线程被分配了优先级,这有助于操作系统对它们的处理进行优先级排序。这是一个基本上自动化的过程,取决于计算机资源的当前状态。
尤其是在大型项目中,多线程绝对需要协同工作。幸运的是,Java非常重视这种方法。线程同步在Java的上下文中是指控制多个线程对一个共享资源的访问。为什么要同步呢?如果没有这种方法,几个线程可能会试图同时访问相同的数据。这不利于数据的排序或稳定性。
我们甚至不必同步整个方法;我们可以选择在哪里使用这项技术。同步代码块将一个监视器锁绑定到一个对象。同一对象种类的所有同步块只允许一个线程同时执行它们。
要添加简单的块级同步,可以用synchronize(this){...}。括号带宾语;在我们的例子中,我们使用关键字this来指代它所属的(目前不存在的)方法。
按照Java的说法,饥饿在一个线程(或一组线程)没有获得足够的CPU资源来正确执行时发生,因为在这种情况下,“果汁”被其他更贪婪的线程霸占了。那些宝贵的CPU资源的平均分配系统被称为公平。Java饥饿的主要原因之一实际上在于基于块的同步。处理饥饿的主要方法是通过适当的线程优先级和使用锁。
现在,Java中的每个对象都有一个所谓的锁属性。这种机制只允许那些被明确授予权限的线程访问对象数据。在这样一个线程处理完一个对象后,它执行一个锁释放,这将锁返回到可用状态。在Java中,使用锁是一种比基于块的方法更复杂的同步形式(见表5-1)。
表5-1
Java中基于块的同步和基于锁的同步的主要区别
块同步
(如同步(this)...)
基于锁的同步
||---|---|---||公平|不支持|支持||等待状态|不能被打断|可以被打断||发布|自动的|指南||可以查询锁定|不|是|
现在是时候回顾一下在Java.lang.Thread中发现的一些关键方法了(见表5-2)。随着我们继续阅读本书,这些方法和许多其他方法将会越来越为你所熟悉。
表5-2
Java的Thread-class(Java.lang.Thread)中的一些方法
方法
示例
为了处理Java中的错误,程序员可以实现try-catch块。监视try块中的代码是否有错误,如果有错误,程序将切换到catch块中的代码。这种方法也被称为异常处理。看一下清单5-2来看看机械师的动作。
publicclassTryCatchDemo{publicstaticvoidmain(String[]args){inthappyNumber=20;try{//Dividetwentywithzero(uhoh..)happyNumber/=0;System.out.println(happyNumber);}catch(Exceptione){//DisplayourerrormessageSystem.out.println("Wehaveanerror.");}}}Listing5-2Asimpledemonstrationofthetry-catchblock(i.e.,exceptionhandling)inJava最后:更多有趣的尝试捕捉块是的,Java的try-catch块带来了更多的乐趣。无论前面的try-catch中是否出现异常,最终都会执行一个名为的可选块;这对于确保在任一事件之后运行任何特定的关键操作非常有用。参见清单5-3中一个在Java中使用finally块的简单例子。
publicclassTryCatchFinally{publicstaticvoidmain(String[]args){//Beginatry-catchblocktry{//happyArrayisallocatedtoholdfiveintegers//(ThecountstartsfromzeroinJava)inthappyArray[]=newint[4];//Benaughtyandassignthevalueof14into//anon-existentsixthelementinhappyArrayhappyArray[5]=14;}catch(ArrayIndexOutOfBoundsExceptione){System.out.println("Weencounteredamedium-sizedissuewithanarray..");}//Implementafinally-blockwhichisexecutedregardlessof//whathappenedinthetry-catchblockfinally{System.out.println("YouarereadingthisinAlanPartridge'svoice!");}}}Listing5-3AJavalistingdemonstratingafinallyblockduringexceptionhandling抛出:您的自定义错误消息为了创建我们自己的异常/错误消息,我们可以在Java中使用throw语句(参见清单5-4)。使用throw,我们可以得到更详细的错误消息,指出代码中有问题的行。
publicclassFruitChecker{//CreatemethodforcheckingtypeoffruitstaticvoidcheckFruit(Stringfruit){//Displayourcustomerrormessageiffruitdoesnotequal"Lemon"if(!fruit.equals("Lemon")){thrownewRuntimeException("Onlylemonsareaccepted.");}else{System.out.println("Lemondetected.Thankyou!");}}publicstaticvoidmain(String[]args){checkFruit("Lemon");checkFruit("Orange");}}Listing5-4AJavalistingdemonstratingtheuseofthethrowstatementJava中的异常基本上是程序执行过程中出错的信号。程序员可以实现多种类型的异常。
在清单5-4中,术语RuntimeException指的是一般类型的错误;我们选择扔掉一个,以防我们不会遇到柠檬。因为我们对“柠檬”和“橙子”都运行了checkFruit()方法,所以我们既得到了无异常的消息,又得到了有异常的消息。参见清单5-4的输出:
检测到柠檬。谢谢大家!
线程“main”Java.lang.runtime异常:只接受柠檬。
atFruitChecker.checkFruit(FruitChecker.java:8)atFruitChecker.main(FruitChecker.java:17)接下来让我们看看Java中其他一些常见的异常:
Java中基本上有两类异常:已检查和未检查。前者用于处理程序无法理解的错误,例如文件操作或网络传输的问题。未检查的异常,有时也被称为运行时异常,处理编程逻辑中的问题,例如无效参数和不支持的操作。参见表5-3了解这两种异常之间的更详细的差异。
表5-3
Java中检查异常和未检查异常的主要区别
检查异常
未检查/运行时异常
||---|---|---||主要作用区域|外部,程序之外|内部,程序内||例题|文件访问或网络连接问题,外部数据库问题|有缺陷的程序逻辑;类定义、算术运算或变量转换的错误||检测|在程序编译期间|在程序执行期间||优先级|关键:不可忽视。程序将无法执行|严重:不应忽视程序将运行,但或多或少仍不稳定|
我们将在本书的后面更详细地讨论Java异常处理。
基本上,文件操作是指读取、写入和删除存储在硬盘或固态硬盘等设备上的数据。为了建立一个准备好文件操作的Java项目,我们应该在清单的开头添加下面的包:importjava.io.File。实际上,让我们看看Java中一个简单的文件操作程序是什么样子的(见清单5-5)。
表5-4
java.io.File提供的Java中文件操作的一些常用方法
言归正传,我们接下来做一个文本文件,好吗?参见清单5-6了解我们下一剂Java文件操作的良药。
接下来,我们有Filewriter类。正如您所料,这个类提供了写入数据和创建新文件的方法。我们从Filewriter实例化一个对象,将其命名为happyfile.txt.
在清单5-6中,我们为我们的文件提供了一个文件名和一个目录路径,happyfile.txt。带有字母C的部分指的是Windows根目录驱动器的最常见选择。您可以在在线编程环境中运行这个列表。然而,如果你使用硬盘,你不太可能在硬盘上写任何东西。
沿着清单向下,我们从Filewriter类调用write方法,将来自字符串数据的消息传递给它。我们的happyfile.txt现在包含了一条基于文本的消息。接下来,我们执行close方法;需要这样做,以便后续方法能够访问happyfile.txt。
现在,我们再次打开我们的文件,这次是为了在屏幕上打印它的内容。我们首先使用Java的File类实例化一个对象,即FileFile1=newFile("c:\\happyFile.txt")。行fileinputstreamfileinput1=newfileinputstream(file1)让我们能够访问Java文件输入流类,以一个字节为增量提供文件读取功能。在我们的例子中,这意味着一次读取和显示一个文本文件(即happyfile.txt)。为此,我们实现了一个while循环,它使用了FileInputStream类中的一个方法(例如,read)。
让我们做一些更多的文件操作。不如我们查询文件属性,比如文件大小,并尝试删除一些文件(参见清单5-7)。
importjava.io.*;classFileStreamTesting{publicstaticvoidmain(Stringargs[]){//Begintry-catchblocktry{//DefineastringweuseasanameforadirectoryStringdirectoryName="HappyDirectory";Filedir1=newFile(directoryName);//CreatenewdirectoryintotheWindowsroot-folder,C:dir1.mkdir();//Createanarrayofsixcharacters,PublishercharPublisher[]={'A','p','r','e','s','s'};//InstantiateanobjectfromtheOutputStreamclass,stream1OutputStreamstream1=newFileOutputStream("c://HappyDirectory//publisher.txt");for(intx=0;x 在指定目录位置时,两个清单有不同的符号:在清单5-7中,我们使用反斜杠(即c:\happyfile.txt)来表示目录路径,而在清单5-8中,我们使用正斜杠,如c://HappyDirectory。这两种方法都可以在Java中使用。 importjava.time.LocalDate;importjava.time.format.DateTimeFormatter;publicclassHappyDateDemo{publicstaticvoidmain(Stringargs[]){//CreateanobjectusingtheJavaLocalDate-classLocalDatehappyDate=LocalDate.now();LocalDatefortnite=happyDate.minusDays(14);LocalDatetomorrow=happyDate.plusDays(14);System.out.println("Today'sdateis"+happyDate);System.out.println("Twoweeksagoitwas"+fortnite);System.out.println("Twoweeksintothefutureit'llbe.."+tomorrow);//CreateanddisplaythreedifferentformattingpatternsSystem.out.println("Today'sdateinformatA:"+happyDate.format(DateTimeFormatter.ofPattern("d.M.uuuu")));System.out.println("Today'sdateinformatB:"+happyDate.format(DateTimeFormatter.ofPattern("MMMduuuu")));System.out.println("Today'sdateinformatC:"+happyDate.format(DateTimeFormatter.ofPattern("d.MMM.uu")));}}Listing5-9AprogramdemonstratingdateretrievalandformattinginJava表5-5 现在,方法setWeekdays()和setMonths()都接受字符串数组,这些字符串数组用于覆盖默认的格式符号(例如,星期一、一月等)。).这些数组中的第一项故意留空,因为它不考虑这些方法的工作。 谈到清单5-12中的格式模式,我们使用E来表示一周中的某天(现在已经修改过了),用M来表示月份,用y来表示年份。我们还在单引号内添加了一些空格和其他标点符号(例如,'!')。 如果我们在2021年6月29日运行清单5-12,我们将得到以下输出: 今天是不温不火的星期二,在快乐的2021年6月! 国际化是指设计软件,使其无需大修即可本地化;本地化是为特定语言和/或地区创建内容的行为。就国际化而言,Java是一种很棒的编程语言,它提供了相当动态地本地化日期、货币和数字的方法。 术语“国际化”输入起来可能有点麻烦。因此,软件人员有时更喜欢缩写i18n而不是。本地化,另一方面,在知情人士中被称为L10n。你可能已经猜到了,这些节略中的数字代表了前面提到的两个术语中的字母数量。 清单5-12中简单提到的JavaLocale类提供了本地化我们项目的选项。它让程序员有机会呈现本地化的消息或程序的其他部分。Locale类用于标识对象,而不是对象本身的容器。 Locale类有三个维度:语言、国家和变体。虽然前两个是不言自明的,但variant用于表示程序运行的操作系统的类型和/或版本。 现在,在Java中,一个应用程序实际上可以同时有几个活动的语言环境。例如,可以组合意大利日期格式和日本数字格式。因此,Java是创建真正多文化应用程序的好选择。让我们看看清单5-13中关于如何在Java中使用本地化的演示。 我们还调用了第四个对象来检查用户操作系统的地区设置。首先,getDefault()方法用于检索这些数据,并将其插入到我们命名为yourLocale的对象中。然后,执行另外两个方法,即getDisplayLanguage()和getDisplayCountry(),来显示我们感兴趣的信息。 第六章将提供一些确实完全不同的东西;我们将探索强大的Python语言的更高级的功能。 在前一章中我们已经深入了解了Java的世界,现在是时候用Python做同样的事情了。我们将从文件操作开始,转到多线程和本章后面的其他更高级的主题。这里的目的是为您提供Python中一些更深层机制的坚实基础,以便您以后根据需要进行构建。 为了在Python中打开文件,我们使用了名副其实的open()函数。它使用以下语法:【文件对象名=打开(文件名,访问模式,缓冲)。最后两个属性是可选的。Python的open()函数的一个简单示例如下: happyfile=open("happytext.txt")现在,Python使用所谓的访问模式进行文件操作。不是所有的文件都需要写入或附加到文件中,有时你需要做的只是从一个文件中读取。Python的文件访问模式列表见表6-1。 表6-1 十二种Python文件访问模式 缓冲在Python的文件操作上下文中,指的是将文件的一部分存储在临时内存区域,直到文件被完全加载的过程。基本上,零(0)值关闭缓冲,而一(1)值启用缓冲,例如,somefile=open("nobuffer.txt","r",1)。如果没有给定值,Python将使用系统的默认设置。通常保持缓冲是一个好主意,这样可以提高文件操作的速度。 Python中基本上有四个主要的文件属性(其中一个大部分是历史感兴趣的,即softspace)。如果您的项目包含哪怕是最少量的文件操作,您可能会对它们都非常熟悉。参见表6-2了解Python中这些属性的概要。 表6-2 四个Python文件对象属性 属性 ||---|---|---||。名字|返回一个文件名|happyfile=open("happytext.txt")打印(happyfile.name)||。方式|返回文件访问模式|anotherfile=open("whackytext.txt")打印(另一个文件.模式)||。关闭的|如果文件关闭,返回“真”|apess_file=open("supertext.txt")apress_file.close()如果打印(apress_file.closed):print("文件关闭!")||。软空间|如果print语句要在第一项前插入一个空格字符,则返回“false”。历史:从Python3.0开始过时|file5=open("jollyfile.txt")print("软空间集?"文件5.软空间)| 文件需要在Python中保持打开状态才能被操作;设置为关闭的文件无法写入或检查其属性。参见清单6-1中关于文件访问以及如何用Python读取文件属性的演示。 #Importaspecialmodule,time,forthesleep()methodimporttimefile1=open("apress.txt","wb")#Create/openfileprint("File-name:",file1.name)#Startreadingfileattributesprint("Openingmode:",file1.mode)print("Isfileclosed",file1.closed)time.sleep(1)#Sleep/delayforonesecond,forthesuspensefile1.close()#Closefileprint("Nowweclosedthefile..");time.sleep(1.5)#Sleep/delayfor1.5secondsprint("Fileisclosednow",file1.closed)Listing6-1AlistinginPythondemonstratingsomebasicfileoperationsPython中的目录操作目录或文件夹是任何操作系统的文件管理系统的重要组成部分;Python也提供了处理它们的方法。接下来让我们看看一些目录操作(参见清单6-2)。 importos#importtheosmodulefordirectoryoperationsprint("Currentdirectory:",os.getcwd())#Printcurrentdirectoryusinggetcwd()print("Listoffiles:",os.listdir())#Listfilesindirectoryusinglistdir()os.chdir('C:\\')#Changetothe(mostcommon)Windowsroot-directoryprint("Newdirectorylocation:",os.getcwd())#Printcurrentdirectoryagainprint("Let'smakeanewdirectoryandcallitJollyDirectory")os.mkdir('JollyDirectory')#Makeanewdirectoryusingmkdir()print("ListoffilesinJollyDirectory:",os.listdir('JollyDirectory'))Listing6-2AlistinginPythondemonstratingdirectoryoperations至于重命名目录,我们会使用os.rename("Somedirectory","Newname")。一旦一个目录不再需要,并且首先是空的文件,只需要OS.rmdir("somedirectory")就可以删除它。 我们有办法在Python中找到匹配特定命名模式的文件。从清单6-3中可以明显看出,这些实现起来相当简单。 importosimportfnmatch#DisplayallfilesinWindowsrootwith.txt/.rtfextensionforfilenameinos.listdir('C:\\'):iffilename.endswith('.txt')orfilename.endswith('.rtf'):print(filename)#Displayallfileswith.txtextensionstartingwith'a'forfilenameinos.listdir('C:\\'):iffnmatch.fnmatch(filename,'a*.txt'):print(filename)Listing6-3AlistinginPythondemonstratingfilenamepatternmatching在清单6-3中,我们引入了两个方便的函数来定位具有特定命名约定的文件,即endswith()和fnmatch()。后者提供了所谓的基于通配符的Python项目搜索(例如,文件)。txt*还是????name.txt)。 术语globbing指的是在特定目录(或者Python的当前工作目录,如果没有指定的话)中执行高度精确的文件搜索。术语glob,是global的缩写,起源于基于Unix的操作系统世界。在清单6-4中,您将看到这种方法得到了很好的应用。 表6-3 Python中一些常见的日期格式标记 一个正则表达式,通常简称为RegEx,是一个字符序列,构成了字符串的搜索模式。导入一个简单的叫做re的代码模块允许我们在Python中执行正则表达式工作。您可以使用正则表达式来查找具有特定搜索模式的文件,以及在众多类型的文本文件中查找特定的术语。参见清单6-6首次演示它们是如何工作的。 importretext1="Apressisthebestpublisher"regex1=re.findall("es",text1)#CreateaRegEx-objectwithsearch-patternprint("Lookingforallinstancesof'es'")print("Wefound",len(regex1),"matchesin",text1)Listing6-6AsimpleexampleofusingregularexpressionsinPython在清单6-6中,我们调用findall方法在字符串变量text1中寻找“es”的实例。我们还使用Python的len()方法对存储在listregex1中的实例进行计数。现在,是时候看看RegEx魔术的另一个演示了(见清单6-7)。 正则表达式实际上可以追溯到1951年,当时是由美国数学家斯蒂芬·科尔·克莱尼(1909–1994)提出的。如今,正则表达式是许多流行的编程语言中的主要部分,包括Perl、C#和Java。后两种语言中的正则表达式实现将在本书的后面部分探讨。 表6-4 一些重要的Python元字符 importrematch1=re.search('.....','Hellothere!')ifmatch1:#Thisisshorthandfor"ifmatch1==True:"print(match1)#Displays"Hello"match2=re.search('\d..','ABC123')ifmatch2:print(match2)#Displays"123"match3=re.search('\D*','MynameisReginald123456.')ifmatch3:print(match3)#Displays"MynameisReginald"match4=re.search('y*\w*','Hello.Howareyou')ifmatch4:print(match4)#Displays"you"match5=re.search('\S+','Hello.Whatsup')ifmatch5:print(match5)#Displays"Hello."Listing6-8AlistinginPythondemonstratingtheuseofmetacharactersinregularexpressions在清单6-8中,对于match4,我们使用元字符\w,它指的是寻找任何完整单词的匹配。如果没有这个字符,我们会看到输出“y”而不是“you” 让我们学习更多关于Python中元字符的知识。参见表6-5中的八个更重要的正则表达式标记,以及清单6-9中的第二个演示。 表6-5 一些更重要的Python元字符 importrestring1='BeezowDoo-dooZopittybop-bop-bop'patterns=[r'Do*',#Dandzeroormoreo's(*)r'Be+',#Bandoneormoree's(+)r'Do',#Dandzerooroneo's()r'it{2}',#iandtwot'sr'[BDZ]\w*',#LookforfullwordsstartingwithB,D,orZr'^Be\w*',#Lookforafullwordstartingwith"Be"r'...$'#Lookforthethreelastdigitsinthestring]defdiscover_patterns(patterns,string1):#Createourmethodforpatterninpatterns:newpattern=re.compile(pattern)#Summoncompile()print('Lookingfor{}in'.format(pattern),string1)print(re.findall(newpattern,string1))#Summonfindall()discover_patterns(patterns,string1)#ExecuteourmethodListing6-9AnotherlistinginPythondemonstratingtheuseofmetacharactersinregularexpressions清单6-9中的列表结构模式包含七个搜索,而字符串1存储我们的源材料。这两个数据结构将被输入到我们接下来创建的方法discover_patterns中。 在我们的这个新方法中,我们使用了Python的两个RegEx函数:compile()和findall()。对于前者,我们将RegEx模式转换成模式对象,然后用于模式匹配。这种方法在重复使用搜索模式的情况下最为有效,比如数据库访问。 Findall用于发现字符串中搜索模式的所有匹配项。清单中的r'让Python知道一个字符串被认为是“原始字符串”,这意味着其中的反斜杠将在没有特殊函数的情况下被解释。例如,\n不会表示原始字符串中的换行符。 接下来让我们探索RegEx的更多高级特性。清单6-10展示了两个新方法的使用:group()和sub()(为了方便起见,以粗体显示)。此外,我们将和我们的老朋友在RegEx中使用一项新技术search()。 importrestring1="Today'sdessert:banana"#Summonsearch()withfouroptionsforamatchchoice1=re.search(r"dessert.*(noni-fruit|banana|cake|toilet-paper)",string1)ifchoice1:print("You'llbehavingthisfor",choice1.group(),"!")string2="Haveagreatday"string2=re.sub('great','wonderful',string2)print(string2)#Outputs:Haveawonderfuldaystring3='whatisgoingon'#ReplacealllettersbetweenaandhwithacapitalXstring3=re.sub('([a-h])s*','X',string3)print(string3)#Outputs:wXXtisXoinXonListing6-10AlistinginPythondemonstratingthesub()method清单6-10中的搜索方法用于比较和定位现在作为方法参数列出的字符串。换句话说,在字符串1中一共搜索了四种水果。它们由逻辑or运算符分隔,用竖线字符表示(即|)。这个方法是用来寻找这些字符串/水果的,但是它们只应该出现在字符串“dessert”的旁边。如果string1和choice1中的一个条目匹配,程序就会显示出来。 并行处理是指同时执行一个以上的计算或指令。你可能还记得上一章中多线程的概念。像Java和C#一样,Python能够处理多个执行线程。然而,还是有一些主要的区别。Python的多线程实际上并不是以并行的方式运行线程。相反,它伪并发地执行这些线程。这源于Python实现了一个全局解释器锁(GIL)。该机制用于同步线程,并确保整个Python项目仅在单个CPU上执行;它只是没有利用多核处理器的全部魅力。 Python中线程的实际并行处理是通过使用多个进程实现的,所有进程都有自己的解释器和GIL。这被称为多重处理。 Python对并发性的理解可能有点复杂(比如说,与Java相比)。现在,Python中的进程不同于线程。尽管两者都是独立的代码执行序列,但还是有一些不同之处。一个进程往往比一个线程使用更多的系统内存。流程的生命周期通常也更难管理;它的创建和删除需要更多的资源。螺纹和工艺对比见表6-6。 表6-6 Python中线程和进程的主要区别(例如,多重处理和多线程) 过程 线 ||---|---|---||使用单个全局解释器锁(GIL)|不|是||多个CPU内核和/或CPU|支持|不支持||代码复杂性|不太复杂|更复杂||RAM占用空间|巨大的|驳船||可以被打断/杀死|是|不||最适合|CPU密集型应用、3D渲染、科学建模、数据挖掘、加密货币|用户界面、网络应用| Python有三个用于同时处理的代码模块:多处理、异步和线程。对于CPU密集型任务,多处理模块工作得最好。 图6-1 使用三个线程的Python多线程的部分可视化 是时候实际一点了。让我们看看多线程是如何在Python中实现的(参见清单6-11)。 importthreadingdefhappy_multiply(num,num2):print("Multiply",num,"with",num2,"=",(num*num2))defhappy_divide(num,num2):print("Divide",num,"with",num2,"=",(num/num2))if__name__=="__main__":#Createtwothreadsthread1=threading.Thread(target=happy_multiply,args=(10,2))thread2=threading.Thread(target=happy_divide,args=(10,2))#Startthreads..thread1.start()thread1.join()#..andmakesurethread1isfullyexecutedthread2.start()#beforewestartthread2thread2.join()print("Alldone!")Listing6-11AlistinginPythondemonstratingelementaryuseofthethreadingmodule我们在清单6-11中创建了两个函数,分别是happy_multiply和happy_divide。每个都有两个参数。对于前者,我们用num乘以num2,而后者用num除以num2。然后结果被简单地打印在屏幕上。 并发处理环境中的竞争条件是指两个或多个进程同时修改资源(如文件)的场景。最终结果取决于哪个进程先到达那里。这被认为是不理想的情况。 图6-2 使用三个进程的Python多重处理的部分可视化 尽管多处理方法通常会产生CPU效率高的代码,但仍然会出现一些小的开销问题。如果Python中的多处理项目有这些问题,它们通常发生在进程的初始化或终止期间(见图6-2)。 现在,为了简单演示Python的多处理模块,请参见清单6-12。 importtimeimportmultiprocessingdefcounter():#Defineafunction,counter()name=multiprocessing.current_process().nameprint(name,"appears!")foriinrange(3):time.sleep(1)#Delayforonesecondfordramaticeffectprint(name,i+1,"/3")if__name__=='__main__':#Definethislistingas"main",theonetoexecutecounter1=multiprocessing.Process(name='CounterA',target=counter)counter2=multiprocessing.Process(name='CounterB',target=counter)counter3=multiprocessing.Process(target=counter)#Nonamegiven..counter1.start()counter2.start()counter3.start()#Thisnamelesscountersimplyoutputs"Process-3"Listing6-12AlistinginPythondemonstratingelementaryuseofthemultiprocessingmodulePython中的每个进程都有一个名称变量,如清单6-12所示。如果没有定义,将自动分配一个通用标签进程-[进程号]。 Python中有三种重要的数据处理机制,它们有时会混淆:iterables、生成器和协程。列表(例如happyList[0,1,2])是简单的可迭代列表。它们可以根据需要随时阅读,不会出现任何问题。列表中的所有值都会保留,直到明确标记为删除。生成器是迭代器,只能被访问一次,因为它们不在内存中存储内容。生成器的创建类似于Python中的常用函数,只是它们用关键字yield代替了return。 当读取大文件的属性时,比如它们的行数,生成器是很有用的。用于这种用途的生成器将产生不是最新的数据,因此不需要,从而避免内存错误并使Python程序更有效地运行。 协程是一种独特的函数类型,它可以将控制权让给调用函数,而无需结束它们在进程中的上下文;协程在后台平稳地维护它们的空闲状态。换句话说,它们被用于合作的多任务处理。 Asyncio是异步输入/输出的缩写,是Python中为编写并发运行代码而设计的模块。尽管与线程和多处理有相似之处,但它实际上代表了一种不同的方法,称为协同多任务处理。asyncio模块使用在单线程中运行的单个进程提供了一种伪并发性。 异步Python项目的主要组件是事件循环;我们从这个结构中运行我们的子流程。任务被安排在一个事件循环中,由一个线程管理。基本上,事件循环是为了协调任务,确保我们在使用asyncio模块时一切顺利。让我们用清单6-13来看看如何实际实现asyncio模块。 第二个协程中的行result=awaitprime_number_checker(y)用于确保第一个函数已完成执行。在异步方法中,Await确实是一个不言自明的关键关键词。 为了让我们的异步清单完全运行,我们需要使用事件循环。为此,我们创建了一个新的循环对象,happyloop,并调用了一个名为asyncio.get_event_loop()的函数。尽管事件循环管理有多种多样的函数,但为了清楚起见,我们在这个例子中只讨论前面提到的方法。 接下来,在清单6-13中,函数run_until_complete实际上执行我们的print_result(y)函数,直到其各自的任务全部完成。最后,我们通过调用close方法来结束我们的事件循环。 Python提供的所有处理方法可能会让你感到困惑。因此,让我们回顾一下所涉及的一些核心概念来结束本章(见表6-7)。 表6-7 Python中并发编程的主要方法 把一个lambda函数想象成一个一次性函数。Lambda函数是无名的,而且通常规模很小。它们的语法简单如下: 下面是一个简单的lambda函数:sum1=lambdax,y:x+y。对于稍微复杂一点的例子,参见清单6-14。 defhappymultiply(b):returnlambdaa:a*bresult1=happymultiply(5)result2=happymultiply(10)result3=happymultiply(50)print("Result1:",result1(6))print("Result2:",result2(6))print("Result3:",result3(6))Listing6-14AlistingdemonstratinglambdafunctionsinPython现在,Python中有三种方法可以很好地处理lambda函数:map()、filter()和reduce()。Filter()获取一个列表,并创建一个包含所有返回true的元素的新列表。Map()还从它处理的iterables中创建新的列表;可以把它想象成一个无循环的迭代器函数。最后,我们有reduce(),它将一个函数应用于一个列表(或其他可迭代的对象)并返回一个值,这有时也将我们从循环中解放出来(参见清单6-15)。 fromfunctoolsimportreduce#Importcodemoduleneededforreduce()#Createalistcontainingsevenvaluesmiddle_aged=[6,120,65,40,55,57,45]#Displaylistbeforemanipulationsprint("Agesofallpeopleinthestudy:",middle_aged)#Filterlistforagesbetween40and59middle_aged=list(filter(lambdaage:age>=40andage<60,middle_aged))print("Middleagedpeople:",middle_aged)#Summonmap()anddoublethevaluesinthelist"middle_aged"elderly=list(map(lambdax:x*2,middle_aged))print("Theywillliveto:",elderly)#Summonreduce()toaddallelementsin"middle_aged"togethertotal_years=reduce(lambdax,y:x+y,middle_aged)print("Theircombinedtimespentalivesofar:",total_years,"years")Listing6-15AlistingwithalambdafunctionappliedonafilteredlistinPython在Python中压缩以著名的服装闭合机制拉链命名,Python中的zip指的是一种将两个或多个可重复元素结合起来的方法。例如,在用Python创建字典数据结构时,压缩是一个很好的工具。不,这种zip与流行的归档文件格式ZIP没有任何关系,我们将在本章的后面回顾这种格式。 现在,Python中的zip函数接受两个可迭代对象(例如列表)并返回一组元组(参见清单6-16)。 #Definetwolistsletters1=['z','a','r','d','o','z']numbers1=[1,2,3,4,5,6]#Createzipobject,zip1,using"letters1"and"numbers1"zip1=zip(letters1,numbers1)foriinzip1:#Displayzipcontentsprint(i)#Definenewlist,"letters2"letters2=['Z','A','R','D','O','Z']#Createsecondzipobject,zip2,using"letters1","numbers1",and"letters2"zip2=zip(letters2,numbers1,letters1)foriinzip2:#Displaysecondzipcontentsprint(i)Listing6-16AlistinginPythondemonstratingthezipfunction关于拉链的更多信息在很多情况下,基本的拉链都不够用。其中之一涉及具有不完整元组的拉链。幸运的是,我们有一个叫做zip_longest的方法。这个方法在代码模块itertools中找到,用您选择的占位符数据填充任何缺失的元素。参见清单6-17进行演示。 fromitertoolsimportzip_longestletterlist=['a','b','c']numberlist=[1,2,3,4]maximum=range(5)#Defineziplengthandstoreitinto"maximum"#Summonzip_longest()zipped1=zip_longest(numberlist,letterlist,maximum,fillvalue='blank')#Displayzipcontentsforiinzipped1:print(i)Listing6-17AlistinginPythondemonstratingthezip_longestmethod你可能想知道Python里的一个拉链能不能拉开。答案是响亮的“是”(见清单6-18)。 letters=['A','B','C','D']numbers=[1,2,3,4]zipped1=zip(letters,numbers)#Zipthedatazipped_data=list(zipped1)print("Here'sthezip:",zipped_data)a,b=zip(*zipped_data)#Unzipthedatausingtheasterisk-operatorprint("Nextcomesthedata,unzipped!")print('Letters:',a)print('Numbers:',b)Listing6-18AlistinginPythondemonstratingunzipping.使用另一个拉链正如本章前面提到的,zip是一个在计算环境中至少有两种流行含义的词。我们探讨了Python中的压缩(和解压缩),涵盖了第一个含义。现在是时候在任何PythonIDE中使用另一种ZIP文件了,这是一种流行的归档文件格式。 现在,用ZIP软件压缩的目录(及其子目录)最终成为一个文件,其大小也往往比未压缩的内容小得多。对于基于网络的文件传输,这显然是一个很好的解决方案。自然,有几种文件压缩解决方案可用,但截至2021年,ZIP仍然是一种无处不在的文件存档格式,可用于所有流行的操作系统。 ZIP是由菲利普·卡兹和的加里·康威在1989年创建的。ZIP压缩技术最近被用作JavaArchive(JAR)文件格式—和许多其他格式的基础。 Python也喜欢它的ZIP文件。我们实际上可以在PythonIDE中操作它们。有一个代码模块被恰当地称为zipfile。参见清单6-19中关于如何创建ZIP文件以及如何在Python中显示其内容的演示。 与ZipFile一起使用的with语句基本上是为了让我们的清单更加清晰。首先,它们让我们不用使用关闭方法(例如,happyfile.close()),这些方法通常在文件操作完成后才需要。 清单6-19将在您当前的Python目录中执行,因此您的结果可能会有所不同。 我们将在本书的后面回到Python语言的复杂性。 第七章将会看到我们探索C#语言更高级的一面。 与Java和Python一样,C#提供了处理日期和日历所需的一切。让我们来看一个访问日期和日历的程序,好吗?(参见清单7-1。) 在C#中,DateTime结构是系统名称空间的一部分,因此不需要以任何其他方式将它们包含在我们的项目中。 表7-1 C#中DateTime结构包含的一些核心属性 C#非常重视全球文化这一概念。全球化是指为全球任何地方的用户设计应用程序。本地化指的是定制应用程序以符合特定文化需求的过程。在实践中,这些概念处理我们星球提供的不同日历、货币、语言和位置。在C#中,我们有大量的方法和属性用于显示本地化信息。他们中的许多人来自系统。全球化名称空间。 C#中的Calendar类包含总共14种不同的日历,以满足您的本地化需求。见表7-2浏览其中的八个。 表7-2 C#中包含的一些日历 C#使用语言文化代码进行本地化。在下面的清单中,我们使用了四个,即en-US、fi-FI、se-SE和es-ES。此代码中的前两个字母代表一种语言,而第二个大写的字母对代表一个特定的地区。例如,德语(de)可以用附加的文化代码表示,例如,de-AT(奥地利)、de-CH(瑞士)和de-LI(列支敦士登)。现在,请看清单7-3的演示。 C#中的语言文化代码基于ISO3166-1国家列表。截至2021年,总共支持249种语言文化代码。 我们采用的解决方案是在显示CultureInfo对象时使用一个foreach元素。C#中的foreach是for循环的替代方法,更适合在数组中迭代。例如,这个元素与CultureInfo的实例配合得非常好。 现在是时候看一个货币格式化的小例子了(参见清单7-4)。 usingSystem.Globalization;publicclassJollyCurrencies{publicstaticvoidMain(){intcash=10000;//Displaycashamountwithoutcurrency-formatConsole.WriteLine("Nocurrency-format:"+cash.ToString());//SetCurrentCulturetoFinnishinFinland.Thread.CurrentThread.CurrentCulture=newCultureInfo("fi-FI");//Addtheappropriatecurrency-formatusing"c"Console.WriteLine("Finnishcurrency-format:"+cash.ToString("c"));//SetCurrentCulturetoBelarusianinBelarusanddisplaystringThread.CurrentThread.CurrentCulture=newCultureInfo("be-BY");Console.WriteLine("Belarusiancurrency-format:"+cash.ToString("c"));//SetCurrentCulturetoChineseinPeople'sRepublicofChinaThread.CurrentThread.CurrentCulture=newCultureInfo("zh-CN");Console.WriteLine("Chinesecurrency-format:"+cash.ToString("c"));}}Listing7-4AlistingdemonstratingcurrencyformatsintheC#CultureInfoclass在清单7-4中,我们分别为芬兰语、白俄罗斯语和汉语创建并显示了三种不同的货币格式。方法ToString("c")用于指定一种货币。 C#通过一个简单的叫做File的类为所有类型的文件操作提供了通用的工具。我们可以读取和显示文本文件,创建新文件,并检索多种类型的属性,如创建日期等等。 在清单7-5中,我们从C#中创建了一个新文件,我们称之为apress.txt。在里面,我们会写一条小信息(“Apress是最好的出版商!”).不止于此,我们将继续读取并显示我们刚刚创建的文件的内容。 要使用File类,我们需要系统。我们程序中的IO命名空间。 usingSystem;usingSystem.IO;classHappyTextWriter{staticvoidMain(string[]args){using(TextWriterhappywriter=File.CreateText("c:\\apress.txt")){happywriter.WriteLine("Apressisthebestpublisher!");}Console.WriteLine("TextfilecreatedinC:\n\nItreads:");//Openthefilewejustcreatedanddisplayitscontentsusing(TextReaderhappyreader1=File.OpenText("c:\\apress.txt")){Console.WriteLine(happyreader1.ReadToEnd());}}}Listing7-5AlistingdemonstratingtheTextWriterandTextReaderclassesinC#除了文本文件,还有另一种文件格式可供我们使用。在清单7-6中,我们将使用另一个主要类型:二进制文件。这种格式非常通用,可以存储文本、数字以及我们可能用到的任何东西。在下一个清单中,我们将创建一个二进制文件,并在其中存储一个浮点数、一个文本字符串和一个布尔变量。同样,我们需要使用系统来利用文件类。IO命名空间。 File类在C#中有一个替代项;FileInfo提供了更多的控制,在某些情况下更有用。让我们用清单7-7来看看如何用FileInfo访问文件属性。 usingSystem;usingSystem.IO;classFileInfoAttributeFun{publicstaticvoidMain(){stringfileName1=@"C:\apress.txt";//SetourtargetfileFileInfoourfile=newFileInfo(fileName1);stringna=ourfile.FullName;Console.WriteLine("Attributesfor"+na+":");stringex=ourfile.Extension;Console.WriteLine("Fileextension:"+ex);boolro=ourfile.IsReadOnly;Console.WriteLine("Read-onlyfile:"+ro);longsz=ourfile.Length;Console.WriteLine("Size:"+sz+"bytes");DateTimect=ourfile.CreationTime;Console.WriteLine("Creationtime:"+ct);DateTimela=ourfile.LastAccessTime;Console.WriteLine("Lastaccess:"+la);}}Listing7-7Alistingdemonstratingfile-attributeaccessusingtheC#FileInfoclass清单7-7希望您在Windows硬盘的根目录下有一个文本文件apress.txt。您可以修改文件名1指向不同的位置和/或不同的文本文件,让清单7-7发挥它的魔力。 现在是时候看看FileInfo可以访问的一些最常用的属性了。纲要见表7-3。 表7-3 C#中FileInfo类包含的一些属性。 接下来让我们来看一个更全面的C#文件操作的例子。在清单7-8中,我们更广泛地使用了FileInfo类。这包括使用CopyTo方法复制文件以及使用Delete方法删除文件。 接下来,我们使用FileInfo类创建一个对象,将其命名为ourfile。然后为文件副本创建另一个对象ourfile_copy。我们需要实例化FileInfo,以便稍后使用类方法。 字符串值前的at符号,即@,表示C#中的文字字符串。这意味着后面的字符串不会被解析为任何转义序列,比如"\\"中的反斜杠。比较下面几行(最上面一行是文字字符串): stringfile1=@"c:\user\xerxes\documents\text\nincompoop.txt"; stringfile1="c:\\user\\xerxes\\documents\\text\\nincompoop.txt"; 这句台词如果(!ourfile。Exists)是检查对象ourfile的不言自明的Exists属性。前面有一个感叹号,条件句是“如果我们的文件不存在。”继续前进,行档。创建(文件名1)。dispose();包含两条指令。首先,Create方法在您的存储系统上创建一个实际的文件。Dispose方法主要释放文件的访问状态。如果没有它,我们可能会在程序后期操作这个文件时遇到问题。 我们现在转到一些用户交互。如果用户希望删除原始文件及其副本,系统会提示用户按“y”(并点击return)。这是通过Delete方法实现的。如果用户输入任何其他字符而不是“y”,文件将保持不变,并显示一条消息“C:\中的文件未删除”而是显示。 C#的File和FileInfo类非常相似。然而,它们在语言中保持独立是有原因的。当需要对一个文件进行多个操作时,fileInfo类是更好的选择,而File最适合单个操作。FileInfo还通过字节大小的文件操作提供了更多的手动控制。 File中的方法是静态的,而FileInfo中的方法是基于实例的。静态方法需要更多的参数,比如完整的目录路径。反过来,在特定的场景下,比如为基于网络的环境编码,稍微更费力的文件类实际上可能产生更有效的结果(见表7-4)。 表7-4 C#中File和FileInfo类的主要区别 文件 文件关于 ||---|---||使用静态方法|使用基于实例的方法||不需要实例化|需要实例化||提供更多方法|提供较少的方法||在某些情况下可以提供更快的性能,例如网络流量|对文件读写操作有更好的控制| 要观察FileInfo提供的一些手动控制,请参见清单7-9。 接下来,使用行inthowmanybytes=(int)bytearray1。长度;我们创建了一个整数,howmanybytes,在其中我们应用了一个长度方法,以便找出我们的文件的字节大小。在随后的while循环中,使用read方法读取字节。 更准确地说,这个while循环将在变量howmanybytes保持大于零时执行。我们之前定义的文件流filestream1应用了Read方法,将字节数组(bytearray1)、目前读取的字节数(bytesread)和要读取的字节总数(howmanybytes)作为该方法的参数。这个输出被输入到变量I中。当I达到零时,我们使用break关键字中断这个循环。 清单7-9的最后步骤包括首先将bytearray1转换成可读的Unicode字符,并将其存储到string1中,然后显示该字符串。 使用UTF-8字符编码,存储在字符串中的每个拉丁字母数字字符通常占用两个字节,即16位。 垃圾收集(GC)的概念基本上是指自动化的内存管理。这种机制可以根据程序的需要分配和释放RAM支持垃圾收集的编程语言将程序员从这些任务中解放出来。包括C#在内的大多数现代语言都支持开箱即用的gC。这个特性也可以以附加软件库的形式添加到没有本地支持的语言中。 随着一个编程项目的增长,它通常包括越来越多的变量和其他数据结构,除非得到适当的处理,否则它们会耗尽RAM。对程序员来说,管理这些资源可能有点麻烦。此外,受损的内存分配会导致稳定性问题。 现在,C#中的垃圾回收在以下三种情况下被调用: 本机堆是由操作系统管理的动态分配的内存。每当一个程序被执行时,本机堆内存被动态地分配和释放。被称为托管堆的内存区域是一个不同的实体。每个单独的进程都有自己的托管堆。一个进程中的所有线程也共享这个内存空间。 如您所料,多线程在C#中确实得到了很好的支持。参见清单7-10进行演示。首先,我们定义一个自定义方法,ChildThread(),来产生新的线程。我们稍后将创建三个线程,它们将同时使用Sleep方法,简单地暂停一个线程的处理。在我们的线程中,这些方法被赋予一个0到6000毫秒之间的随机值,最大延迟为6秒,按照基于线程的范例,所有这些方法都被同时计数。 程序由进程组成。线程是进程内部的实体,它们可以在最需要的时候被调度。在C#中,我们也可以对线程的创建和其他生命周期事件施加很多控制。 接下来,在我们的自定义方法中,我们检索一个相当冗长的属性,称为System。threading.thread.currentthread.managedthreadid为每个正在运行的线程提供一个唯一的标识号,将其存储到happyID中。 设置线程信息时,变量delay除以一千(即乘以0.001)以秒为单位显示。我们还利用C#数学类中的Round方法将延迟整数舍入到两位小数。 像Java和Python一样,C#为它的线程化应用程序提供了锁定机制。有了锁,我们可以让线程同步(参见清单7-11)。 usingSystem;usingSystem.Threading;publicclassLockingThreads{publicvoidOurThread(){//Getahandleforthecurrentlyexecutingthread//sowecanretrieveitsproperties,suchasNameThreadwackyhandle=Thread.CurrentThread;//Applyalocktosynchronizethreadlock(this){//LockedcodebeginsConsole.WriteLine("(Thisthreadis"+wackyhandle.ThreadState+"with"+wackyhandle.Priority+"priority)");for(inti=0;i<3;++i){Console.WriteLine(wackyhandle.Name+"hasbeenworkingfor"+i+"hours");Thread.Sleep(400);//Waitfor0.4secondsbeforenextline}}//Lockedcodeends}}publicstaticvoidMain(){LockingThreadsjollythread=newLockingThreads();Threadthread1=newThread(newThreadStart(jollythread.OurThread));Threadthread2=newThread(newThreadStart(jollythread.OurThread));thread1.Name="Graham";thread2.Name="Desdemona";//Bothofthesethreadsaresynchronized/locked://thread1willbeprocesseduntilcompletionbeforethread2beginsthread1.Start();thread2.Start();}Listing7-11AlistinginC#demonstratingthreadlockingandaccessingsomethreadproperties清单7-11的输出如下: Sleep方法实际上可以在一定程度上模拟C#的产出。年纪大。NET框架(4.0版之前)还不支持Yield。在这些情况下,在Sleep(0)中键入类似于Yield。 现在让我们看看Yield在实际中的表现(参见清单7-12)。 同样,操作系统是根据其他线程的状态和优先级调度发出让步请求的线程的最终实体。 C#中线程工作最重要的方法之一叫做Join。使用这种方法,您可以让线程在其他线程完成处理时等待。自然,Join只能被已经开始执行的线程调用(参见清单7-13)。 usingSystem;usingSystem.Threading;staticvoidOurFunction(){//Createacustommethodfor(inti=0;i<10;++i)Console.Write(i+"");}staticvoidMain(string[]args){Threadthread1=newThread(OurFunction);thread1.Start();thread1.Join();//Join()makessureathreadcompletesitsprocessing//beforethelistingproceedsConsole.Write("10");//Finishthelistwithanumberten}Listing7-13AbasicdemonstrationoftheJoinmethodinC#清单7-13为我们提供了一个Join方法的基本示例。清单7-13的输出应该是012345678910。如果没有Join,您可能会得到意想不到的结果,例如100123456789。这是因为最后的控制台。Write没有被明确告知要等到thread1完成其处理。 基本上,异步编程是一门艺术,让一个程序包含许多任务,这些任务不会互相冲突(或与其他程序冲突)。这种方法对程序员来说也更容易理解,因为它产生了相当清晰的代码布局。 C#中异步处理的当前实现利用了一个名为ThreadPool的类。这个类采用了不同于Thread类的方法(在本章前面已经讨论过)。首先,ThreadPool只创建低优先级的后台线程;给线程分配优先级不是这个类的一部分。 一个设计模式是一个可重用的解决方案,用于解决软件设计中特定环境下反复出现的问题。 现在,C#中有三种主要的异步编程范例。其中只有一个TAP被微软推荐在2021年使用。 有关使用TAP的异步模式的示例,请参见清单7-14。 async/await机制允许程序在后台处理潜在的繁重计算,这通常会减少用户界面的故障和/或加快网络文件传输。 创建方法时,TAP有一些特定的关键字。任务是用来表示不返回值的方法。在我们的清单中,您还会发现一个任务 命令等待任务。Run使用ThreadPool类在自己的线程中启动一个任务。现在,基本上有三种方法来调用这个机制: 在斐波纳契数列中,每个数字代表它前面两个数字的和,例如,0,1,1,2,3,5,8。这个概念是由意大利数学家莱昂纳多·博纳奇(约1170年,可能是公元1250年)在其开创性的著作LiberAbaci中引入的。在他生命中的某个时刻,波纳奇被取了一个绰号叫斐波那契。 读完这一章,你可能会对以下内容有所了解: 第八章致力于C#、Java和Python提供的更好的技术,包括高级多线程。我们将进入制作有用的(但绝对是小规模的)应用程序的世界。 我们将从向世界介绍通用聊天模拟器开始这一章。这个程序将展示Python的以下特性,所有这些我们在本书前面都讨论过: 像本章中的其他程序一样,通用聊天模拟器(UCS)是一个控制台应用程序,这意味着它只在文本模式下工作;为了保持简单,不使用图形元素。 现在,UCS的两个关键元素以虚拟聊天主持人和他们的虚拟观众的形式出现,后者由一个共享的昵称“聊天”来描述。这两个虚拟角色每隔几秒钟就在屏幕上输出文本,文本的内容由三个外部的文本文件决定,因此很容易编辑。 开始使用UCS时,不需要键入整个清单;这个文件可以在GitHub上找到,这个免费下载的列表也有完整的注释。然而,为了彻底起见,现在让我们一个块一个块地检查程序。 只需在macOS终端、Windowsshell或Linux命令行中输入“pip3installclrprint”即可安装clrprint。 我们还在函数中使用了一个临时列表结构templist。数据被输入到templist中,以便可以对其进行枚举并删除其换行符。使用Python的readlines方法读取文件内容。 defopen_chatfile(name,deletechar):try:file=open(name,'r')#openfileinread-onlymode,"r"print("Loadingfile"+name)templist=file.readlines()#readfileintotemplistfori,elementinenumerate(templist):#enumeratethelisttemplist[i]=element.replace(deletechar,'')#deletenewlinesreturntemplist#returntheenumeratedlistfile.close()#closingthefileisalwaysagoodpracticeexcept(FileNotFoundError,IOError):#handleanexception#str()convertsvariablestostringprint("Thefile"+str(name)+"couldnotberead!")Listing8-2ThesecondpartofthelistingfortheUniversalChattingSimulatorinPython第二部分:展示高分在我们开始记分之前,我们要定义三个列表用于open_chatfile函数;他们被称为中性情绪、坏情绪和聊天情绪。来自文本文件的数据需要存储在某个地方,这就是我们三个列表的用武之地(参见清单8-3)。 在Python中,我们可以使用赋值操作符在同一行用同一个变量初始化变量,包括列表,例如,a=b=10或happy=wonderful=[]。 接下来我们实际使用我们的方法,召唤它三次打开文件neutral.txt、bad.txt和chat.txt。open_chatfile的另外两个参数是我们之前创建的列表和换行符。现在,我们有三个列表,充满了发人深省的交流,为我们的虚拟聊天乐趣做好了准备。 在项目的这一部分结束时,我们将打开一个包含当前最高分的文本文件。是的,我们在通用聊天模拟器中有一个评分系统;稍后将详细介绍。我们通过创建一个try块来获取分数数据。提醒一下,try块允许我们动态测试代码中的错误。加上except关键字(exception的缩写),我们可以引入自己的错误消息。在清单8-3的try块中,试图加载文件highscores.txt。如果找不到这样的文件,异常块会告诉我们并创建文件。 虚拟聊天者使用简单称为chat的变量在程序中获得他们的表示;从chatmoods列表中随机选择一个元素,并输入到该变量中,以在清单8-4中进一步显示。 你可能想知道关于情绪多变的喧闹是怎么回事。不仅mood用于中断主循环,而且如果它低于30,程序将开始为我们的虚拟主机使用一组不同的行(即文件bad.txt中定义的更糟糕的行)。mood整数的值受特定关键字的影响,即另外两个变量negativetrigger和positivetrigger,在前面的清单8-1中定义。如果程序检测到一个负面的触发词,“情绪”会减少1到5之间的一个数字。类似地,正触发字导致整数被加上一个1到5之间的数。为此,我们使用行random.randint(1,5)生成一个在期望范围内的整数,将其赋给另一个变量moodchange。 现在,如果在清单8-4中检测到一个触发字,有两个布尔变量会产生一个标志;这些简称为负和正。这两个布尔值用来显示程序后面任何情绪变化的状态消息。 接下来,我们在程序中加入一些修饰性的变化。清单8-4可以用三种格式显示所有的聊天行:常规、首字母大写和全部大写。这是通过每个主循环检查一次变量cap的随机值来实现的。我们发布cap四个潜在值:-1、0、1和2。如果cap保持在1以下,一行颤振保持不变。值1使程序调用Python的大写()方法,正如它的名字所暗示的那样。最后,值2对应于upper()方法,基本上为我们的虚拟chatters打开了capslock(仅针对一行)。 程序中的评分是基于虚拟聊天者发送的聊天消息的数量;每条信息值三分。这个模拟器程序有两种结局。在“坏”的一个中,观看者被简单地告知虚拟聊天主持人的情绪降到了零。另一个更乐观的结局给予观众价值1000分的“欣快奖励”。 表8-1 通用聊天模拟器使用的一些Python方法 清单8-4中的使用示例 ||---|---|---||打开()|打开文件进行读取和/或写入|文件=打开(名称,'r')||阅读()|读取打开文件的内容|hiscore=scorefile.read()||seek()|设置文件的位置|scorefile.seek(0)||random.randint()|返回一个随机整数|neutralmoods[random.randint(0,len(neutralmoods)-1)],||random.choice()|随机选择一个元素|response=random.choice([对,错])||str()|将变量转换为字符串|print("SCORE:"+str(score))||int()|返回一个整数对象|如果score>int(hiscore):||len()|返回对象/字符串长度|| 为了让你回忆一下线程在C#中是如何工作的,请看一下清单8-6。在这个小清单中,发送了三个线程来运行五米短跑。该程序演示了C#语言的以下功能: 现在,我们在清单8-6中精心制作了一个我们自己的方法;这将是的成名之作。这个方法是用来演示数组的,更具体地说是字符串类型的数组。这些数组用名称填充,并作为单个字符串返回,供main函数使用。 ThreadRacer中的f_index和l_index变量用于存储字符串数组first_names和last_names的所需索引位置。这些索引位置是通过在数组长度上应用下一个方法(一个随机数发生器)依次创建的。这些长度然后通过调用恰当命名的长度方法来推导。 下面是清单8-6中的一行代码: intf_index=random0.Next(first_names.Length);这是这样的:“让整数f_index等于一个随机数,其最大值为名字数组的长度。”脱口而出。 我们可以将方法的输出直接添加到变量中。以清单8-6中的这一行为例:线程1。name="[RacerA]"+MakeNames();其中我们将字符串“[RacerA]”与输出中发生的任何MakeNames结合起来,只使用了一个简单的加号运算符。 ThreadRacer的线程调度也是由操作系统执行的。这意味着过一会儿你可能会得到同样的结果。 接下来,清单8-7向您展示了一个基于控制台的小测验程序。它展示了这种优秀语言的以下特征: JollyQuiz使用两个独立的文件,questions.txt和answers.txt。它们都是自上而下处理的,问题文件中的每一行都与答案文件中的同一行相对应。这种方法使添加新问题和修改现有问题变得容易,只需编辑这两个文本文件。 希望用户在程序执行期间键入他们的答案。一个线程对象在后台运行,独立于正在进行的测验,告诉用户“快点!”每四秒钟。每个正确答案加十分。在测验结束时,将向用户显示他们的分数以及正确答案的百分比。 接下来我们将带着一个名为美味芬兰食谱的小程序回到Java的世界。这个相当简单的清单基本上显示了容易定制的文本文件。 清单8-8中有一个try-catch块,如果找不到文件,它会发出声音,也就是抛出一个异常。无限while循环使程序一直运行,直到用户在键盘上输入“4”,在这种情况下,执行break关键字。 在第二个循环中,我们使用带有Thread.sleep(1000)的一行来体验期望的延迟量。请注意,为了让sleep方法在Java中工作,您需要检查它是否有异常。在清单8-9中,我们为此使用了一个经典的try-catch块。 读完本章后,您将会对Python、C#和Java的以下方面有所了解: 第九章的代码会轻得多,都是关于一些可爱的图,也就是那些用通用建模语言(UML)创建的图。 这一章致力于统一建模语言的奇迹,它是软件设计中一个相当普遍的工具——也当之无愧。在本书的前面,我们仅仅触及了UML的表面,现在我们将更深入地探索UML提供的更多可能性,因为它与类建模有关。 通用建模语言有两个标准,即UML1.x(最初于1996年)和UML2.x(于2005年首次发布)。这两个标准都包含许多类型的图,几乎可以用于任何建模目的。 UML图类型的整个范围超出了本书的范围。然而,知道UML有哪些变种是很有用的。概括地说,它通常分为两个主要类别,然后再分为许多子类。许多较大的项目可能需要以下大部分(非详尽的)图表列表;它们是互相排斥的。 如前所述,UML可以用来建模几乎任何东西,一个类图可以帮助我们可视化一个面向对象的软件项目。让我们从简单的事情开始(见图9-1)。 图9-1 一个简单的UML类/对象图 图9-1展示了UML中的类,包括它们的变量和方法以及类间关系;它还具有一个单一的UML对象图。 图9-1中的主类叫做Book。它有三个变量(即标题、出版商和出版年份)。如您所见,我们还需要在类中指定变量的类型(例如,String)。UML中属性或方法前面的加号(+)表示公共访问修饰符。带有破折号(-)字符的类成员表示私有访问修饰符。 你可以像我们在图9-1中所做的那样,直接在UML图中添加有用的注释;这些将采取右上角折叠的矩形的形式。 现在,简单的线条标记了UML中类和其他实体之间的关联。图9-1中这些行旁边的数字和星号是UML中多样性的演示。这个概念用于指示一个类可以提供或被允许与之交互的实例(即对象)的数量。 转到图9-1的对象部分,我们有一个主类的实例,叫做exampleBook1。在UML中,对象可以被表示为尖边或圆边的盒子;为了多样化,我们选择了后者。 继承可以在UML中以简洁的方式表示。为此,我们可以使用基于树的方法(参见图9-2)。 图9-2 使用基于树的方法演示UML中的继承的图表 图9-2中的图表展示了三个专业化水平。首先,您有通用的业务类。接下来是三个专业类别,即出版商、面包店和香水店。最后,我们有两个不同类型的出版商类最专业的水平,一类是婚礼面包店和奢侈品香水店。 在OOP中,术语基类也被称为父类。子类通常被称为子类。 图9-2中的业务类定义为摘要;在UML中,斜体的类名表示这一点。这些类为子类提供了特定的继承方法。抽象类不能直接实例化。相反,您使用它们的非抽象(即具体)子类之一来创建对象。 如果将图9-2翻译成Java会是什么样子?看看清单9-1中可能的解决方案。 在清单9-1中,业务是基类。所有其他的类都被定义在它的文件Business.java中,以避免拥有多个源文件(例如,,等等)。).** 表9-1 清单9-1和9-2的主要区别 元素 清单9-1(Java) 清单9-2(C#) 现在来点不同的。让我们看一看Python中的图9-2可能是什么样子(参见清单9-3)。为了在Python中使用抽象类,我们需要导入一个名为ABC的代码模块。然后,我们让我们的基类Business继承这个模块。行@abstractmethod是所谓的Pythondecorator。你可能已经猜到了,它告诉我们一个方法应该被认为是抽象的。 图9-3 用UML展示聚合关系的图表 接下来,让我们探索如何用UML建模一个基本的踏板驱动的车辆。图9-3中引入了新元素。这是由空心菱形符号描绘的集合体。 聚合关联意味着一个类可以没有其他类而存在。为了拥有一辆功能齐全的自行车,我们需要所有的部件。但是,即使我们移除了其他组件,它们仍然会存在。 坚持使用Python,让我们对图9-3创建一个可能的编程解释(参见清单9-4)。 现在让我们用复合关联和聚合关联来建模。这是为了让你考虑你需要在UML中区分这两种类型的关联的场景。 图9-4代表一台典型的个人电脑。它旨在描述一个功能系统所需的所有主要组件。然而,使用复合关联(即,实心菱形符号)仅描绘了这些组件中的一些。这是因为,从理论上讲,一台个人电脑没有它们也能正常运行。这可能不是最有用的系统,但至少它可以打开并显示错误信息。 图9-4 展示UML中组合关系的图表 图9-4中用复合关联描述的基本计算机组件: 用集合关联描述的不太重要的组件: 请参见表9-2了解聚合和复合关联之间的差异。 表9-2 UML中聚合关联和复合关联的主要区别 总计 复合材料 ||---|---|---||关联强度|无力的|强烈的||关系|"有一个"|“的一部分”||属国|无论超类存在与否,子类都可以存在|子类需要一个超类才能存在||例子|即使一个班级的学生毕业了,大学依然存在关闭单个部门不会终结整个公司|没有了的房子,房间就毫无意义一个功能正常的心脏对于一个人来说是必须的| 实现指的是与两组元素的关系,其中一组代表一个规范,另一组代表它的实现。实现的实际工作取决于上下文,并没有在UML中严格定义(见图9-5)。 图9-5 UML中简单实现关系的三个例子 在图9-5的图表中,你会看到UML实现关系的三个简单例子。在第一种情况下,搜索算法为在线搜索引擎提供功能。同样,显卡需要显卡驱动软件才能真正显示任何图像。最后,支付事件可以通过三种可用的支付方式实现:现金、卡或支票。 接下来,我们有依赖(见图9-6)。这种类型的图表表示从属元素和独立元素之间的连接。用虚线和简单的箭头表示,依赖关系本质上是单向的。 图9-6 UML依赖的两个例子 一个自反关联用来表示属于同一个类的实例。当一个类可以被分成许多责任时,我们可以使用这种类型的关联。 在图9-7中,我们有一个名为雇员的类,它与自己有一个关联,正如现在熟悉的简单线条所描绘的。Employee类用来代表受人尊敬的主管和地位低下的学员;您会注意到UML多样性信息也被插入到这个图中。 图9-7 UML中自反关联的一个例子 现在,图9-7以两种不同的方式描绘了相同的设置。左图显示了基于角色的方法,因为我们展示了两种不同的角色(即主管和学员)。右图使用了一个命名的关联。 在这一点上,回顾一下我们到目前为止遇到的UML元素是一个好主意(见图9-8)。 图9-8 显示UML类图中使用的大多数基本元素的图表 尽管一些UML图类型超出了本书的范围,但是还有一种类型您应该熟悉。这是用例,它以最简单的方式展示了用户与系统的交互。这些通常是包含很少技术细节的高级图表。 我们现在将引入一些新的UML元素。这些是角色、系统和用例。此外,用例图用熟悉的简单线条来描述关系。请看图9-9中的简单用例图。 图9-9 一个描述书籍写作过程中章节回顾的UML用例 你会看到图9-9中的两个演员,由一些可爱的简笔画代表。保持演员在本质上的明确性通常是一个好主意,就像命名一个作者作者而不是约翰或旋律羊毛索克斯。在UML中,发起交互的参与者被称为主要参与者。次要角色对主要角色的行为做出反应。 系统的第二个元素在UML中用一个简单的矩形表示;在图9-9的情况下,称为章审。在系统内部,我们有内部的用例,用椭圆形表示。这些代表了完成特定用例所需的不同阶段。最后,我们用熟悉的简单线条来表示参与者和用例的不同关联。 现在,图9-9中的图表描绘了以下事件顺序: 是时候看看一个稍微复杂一点的用例来引入两个新元素了:扩展了,包含了关系(见图9-10)。正如UML类图的情况一样,我们也可以将那些有用的注释元素合并到UML用例中。 图9-10 描述电子邮件服务基本功能的UML用例 图9-10描绘了一个简单的电子邮件用例场景。我们将账户所有者(即人类用户)作为主要参与者,将电子邮件服务器作为次要参与者。图9-10包含以下事件序列: 除了纸和笔,如果你想尝试在你的计算机上绘制UML,有很多很棒的免费选项。我们现在将回顾这些工具的一些最好的例子。 一个免费且精彩的开源工具,UMLet将让你用其直观的拖放式用户界面立刻绘制出时髦的类图。关联线很好地捕捉到类的边缘并保持在那里,使组织你的图变得轻而易举。 UMLet的特点是为类图、用例、时序图和许多其他的可视化元素提供现成的选择。该计划出口到许多常见的图像文件格式,包括PNG,GIF和SVG。对于那些需要一个漂亮、干净的UML设计工具的人来说,UMLet绝对是必备的。本书中的所有图表都是用UMLet创建的。 在UMLet中发现的一个有趣的特性是它的半自动图表创建。通过导航到文件从文件或目录生成类元素,您可以设置您的UML图的初始阶段。不幸的是,当使用这个特性时,UMLet在创建关联方面有一些问题。 UMLetino是同一软件的基于浏览器的版本。它拥有离线版本的大部分功能。此外,UMLetino还集成了Dropbox,用于导入和导出图文件。但是,该软件仅支持PNG图像文件的输出。 由专门的团队积极维护,Diagrams.net是一个通用的开源工具,有浏览器版和桌面版。后者不需要用户注册。Diagrams.net既面向基本的UML工作,也面向高级用户,具有像层和“插件”这样的特性来增加功能。 以前被称为“draw.io”,该软件在导出UML图(包括AdobePDF)时提供了强大的文件格式支持。不仅如此,通过选择文件导出为高级,你可以设置诸如DPI(每英寸点数)和边框宽度等属性。 从2001年开始开发,Umbrello对于任何类型的UML工作来说都是一个伟大的工具。该软件有一个直观的用户界面,以及从UML生成代码的选项。Umbrello可以将你的图导出为(通常)函数式Java、C#、Python和许多其他编程语言;只需导航到代码代码生成向导,设置您的选项,然后单击下一步。该软件还提供了出色的格式化功能,包括让您正在处理的图表可以使用您的所有操作系统字体。 不要让花哨的默认配色方案欺骗了你——对于许多类型的图表工作来说,Umbrello是一个非常有用的应用程序。 读完这一章,你已经获得了以下知识: 所以我们到了这本书的结尾。过去你可能会觉得可怕的编程术语已经变得不那么可怕了。到目前为止,变量、类和对象对您来说至少已经有点熟悉了。您知道如何用Java、C#和Python制作简单的基于文本的应用程序。或许你甚至可以在聚会上随口解释一下Python对缩进的严格要求。但是你的程序员之旅还远没有结束。了解了三种语言的编码基础,现在你可以开始在这个领域获得更多的经验。一个解决问题的世界在等着你。 编程可能会令人沮丧。但是,让一个讨厌的、不起作用的清单最终发挥作用有一种独特的满足感。无论你最终是否在专业环境中编码,编程在某些时候可能会成为一种建设性的嗜好。它也是治疗失眠和/或无聊的良方。 阿达·洛芙莱斯是勋爵和拜伦女士的私生子,被认为是世界上第一批程序员之一。她与她那个时代的几位杰出科学家一起工作,包括查尔斯·巴贝奇和迈克尔·法拉第,在某个时候,她编写了被认为是有史以来第一个计算机程序。我们让阿达来说最后一句话: 我可以把来自宇宙四分之一的光线投射到一个巨大的焦点上。我的路线是如此清晰明了,以至于想到它有多直就令人愉快。