
在技术的世界中,一直在路上~
虽然前段时间ARM被日本软银收购了,但是科技是无国界的,所以呢ARM相关知识该学的学。现在看ARM指令集还是倍感亲切的,毕竟大学里开了ARM这门课,并且做了不少的实验,当时自我感觉ARM这门课学的还是可以的。虽然当时感觉学这门课以后似乎不怎么用的上,可曾想这不就用上了吗,不过之前学的都差不多忘了,还得捡起来呢。ARM指令集是精简指令集,从名字我们就能看出指令的个数比那些负责指令集要少一些。当然本篇所涉及的ARM指令集是冰山一角,不过也算是基础,可以阅读Hopper中的汇编了,实践出真知,看多了自然而然的就会了。 一、Hopper中的ARM指令 ARM处理器就不多说了,ARM处理器因为低功耗等原因,所以大部分移动设备上用的基本上都是ARM架构的处理器。当然作为移动设备的Android手机,iPhone也是用的ARM架构的处理器。如果你想对iOS系统以及你的应用进一步的了解,那么对ARM指令集的了解是必不可少的,ARM指令集应该也算得上是iOS逆向工程的基础了。 当你使用Hopper进行反编译时,里边全是ARM的指令,那是看的一个爽呢。下面就是使用Hopper打开MobileNote.app的一个Hopper的界面。从主窗口中可以看到全是ARM的指令呢,如果你对ARM指令不了解,那么如何进行分析呢,对吧。所以对ARM指令的了解,是iOS逆向工程的基础呢。今天这篇博客就总结一下ARM指令集的基础指令。 Hopper的功能是非常强大的,在Hopper中你可以对ARM指令进行修改,并且生成一个新的可执行文件。当然Hopper强大的功能可以帮助你更好的理解ARM汇编语言的业务逻辑,Hopper会根据ARM汇编生成相关的逻辑图,如下所示。从下方的逻辑图中你就能清楚的看到相关ARM汇编的指令逻辑。红线表明条件不成立时的跳转,蓝线则表明条件成立时的跳转。 Hopper的功能强大到可以将ARM汇编生成相应的伪代码,如果你看ARM指令不直观的话,那么伪代码对你来说会更好一些。下方就是Hopper根据ARM指令生成的伪代码,如下所示。 貌似有点跑偏了,今天的主题是ARM指令集,Hopper的东西就不做过多赘述了。 二、ARM指令集综述 ARM指令主要是对寄存器,栈、内存的操作。寄存器位于CPU中,个数少速度快,ARM指令集中大部分指令都是对寄存器操作,但有些指令是对栈和内存的操作。下方会对操作栈、寄存器以及内存的指令进行介绍。 1.栈操作---- push 与pop 先简单的聊一下栈的概念,“栈”说白了就是数据结构的一种,栈的数据结构具有LIFO(last in first out) ---- 后进先出的特点。栈在ARM中所指的其实是一块具有栈数据结构特点内存区。栈中主要用来暂存寄存器中的值得,比如R0寄存器正在使呢,可是现在有一个优先级比较高的函数要使用R0, 那么就先把R0的值Push到栈中暂存,然后等R0被优先级更高的函数使用完毕后在从栈中Pop出之前的值。在函数调用时一般会对栈进行操作。 对栈操作的命令就是push和pop了,一般会成对出现,在函数开始时将该函数执行时要使用的寄存器中的值push入栈,然后在函数结束时将之前push到栈中的值在pop到相应的寄存器中。 下方就是push和pop的用法的一个实例。在下方函数开始执行前,将该函数要使用的寄存器r4, r5, r7, lr使用push进行入栈操作,lr是该函数执行后要返回的地址。在函数执行完毕后,使用pop命令将函数执行前入栈的值在pop到相应的寄存器中。有一点需要注意的是将lr寄存器中的值在函数结束后pop到pc (Program Counter)寄存器中,pc寄存器中存储的是将要执行的命令的地址。这样一来,函数执行后就会返回到之前执行的地址上继续执行。 2. pc寄存器中的中的标志位 此处我们以32位指令为例,pc寄存器中的后四位是标志位,第28 - 31位分别对应着V (oVerflow),C (Carry),Z (Zero),N (Negative)。下面分别来介绍一下这四种符号所表示的状态。 N (Negative): 如果结果是负数则置位。 Z (Zero): 如果结果是零则置位。 C (Carry): 如果有进位则置位。 V (Overflow): 在发生溢出的时候置位。 3. 命令操作符 下方是ARM指令集中常用的算术操作: (1)加法操作 ADD R0, R1, R2 ; R0 = R1 + R2 上面的命令就比较简单,就是讲两个数值进行相加。 ADC R0, R1, R2 ; R0 = R1 + R2 + C (Carry) 带进位的加法,ADC将把两个操作数加起来,并把结果放置到目的寄存器中。ADC使用了C--进位标志,这样就可以做比32位大的加法了。下方就是128位的数字进行加法操作的汇编代码。 我们现在要对一个128位的数字进行加法操作,因为我们使用的是32位的寄存器,所以要存储一个128位的数字,我们需要4个(128 / 32 = 4)寄存器。所以我们假设R0,R1,R2,R3寄存器中分别由低到高存储着第一个数字,而R4, R5, R6, R7存储着第二个数字。下方就是两个128数字相加操作的ARM汇编指令。我们将结果存储在R8, R9, R10, R11这四个寄存器中。首先我们执行的是将两个数的最低位相加并设置C标志位(ADDS R8, R0, R4),然后在进行下一位的操作,对R1和R5中的值进行相加,在相加后再加上上次操作的进位,然后再设置标志位,以此类推。这样我们最终的值就存储在了R8-R11这四个寄存器中。 (2)减法操作 SUB R0, R1, R2 ; R0 = R1 - R2 这个命名比较简单,就是使用R1寄存器中的值减去R2寄存器中的值,然后存储到R0中。 SBC R0, R1, R2 ; R0 = R1 - R2 - !C 带借位的减法,假如我们当前的寄存器是32Bit, 如果两个64bit的数值进行减法操作就要使用到SBC借位操作。因为当两个数值在进行减法操作时,如果需要借位时就会把C标志位进行清零操作,所以在进行SBC操作时需要将C标志位进行取反操作。下面我们一128位数值相减为例。该实例与上述的ADC命令类似,在此就不做过多赘述了。 RSB R0, R1, R2 ; R0 = R2 - R1 反向减法 RSC R0, R1, R2 ; R0 = R2 - R1 - !C带借位的反向减法,上面这两个命令与SUB和SBC命令差不多,都是进行减法操作的,不过操作数的计算顺序不同。 (3)、乘法指令 在ARM指令集中,乘法指令有两种第一个是MUL, 第二个是带累加的乘法MLA。当然,这两个指令使用起来都不复杂。 MUL: 乘法指令 MUL{条件}{S} R0, R1, R2 ;R0 = R1 * R2 MLA: 乘法累加指令 MLA{条件}{S} R0, R1, R2, R3 ;R0 = R1 * R2 + R3 (4)、逻辑操作 逻辑操作比较好理解一些,与我们编程中使用的逻辑操作大同小异,无非是一些与、或、非、异或这些操作。 AND R0, R1, R2 ; R0 = R1 & R2 与操作, 1 & 1 = 1, 1 & 0 = 1, 0 & 1 = 1,0 & 0 = 0; ORR R0, R1, R2 ; R0 = R1 | R2或操作, 1 | 1 = 1, 1 | 0 = 1, 0 | 1 = 1, 0 | 0 = 0; EOR R0, R1, R2 ; R0 = R1 ^ R2异或,1 ^ 1 = 1, 1 ^ 0 = 0, 0 ^ 1 = 0, 0 ^ 0 = 1; BIC R0, R1, R2 ; R0 = R1 &~ R2 位清除指令,现将R2进行取反,然后再与R1进行与操作。R1 & (~R2) 将R0的后四位清零:BIC R0, R0,#0x0F MOV R0, R1 ;R0 = R1赋值操作,将R1的值赋给R0 MVN R0, R1 ;R0 = ~R1按位取反操作,将R1的每一位进行取反操作,然后赋值给R0 4、寄存器的装载和存储 有时我们需要将内存中的数据装载到寄存器中进行操作,或者将寄存器中运算后的数据存储到内存中,此时我们就会用到寄存器的装载和存储的相关命令。下方就一一的总结了这些命令。 (1)、传送单一数据 LDR{条件} Rd, <地址> ;将地址中的数据加载到Rd寄存器中 STR{条件} Rd, <地址> ;将寄存器Rd中的数值存储到<地址>中的内存中 LDR{条件}B Rd, <地址> ;将内存地址所对应值得低8位加载到Rd的寄存器中。 STR{条件}B Rd, <地址> ;将寄存器Rd的后8为存的到内存地址中。 LDR (Load Register) : 将数据从内存中取出,加载到寄存器。 LDR Rt, [Rn], #offset ;Rt = *Rn; Rn = Rn + offset LDR Rt, [Rn, #offset]! ; Rt = *(Rn + offset); Rn = Rn + offset STR (Store Register): 将寄存器中的数据,存储到内存。 STR Rt, [Rn], #offset ;*Rn = Rt; Rn = Rn + offset STR Rt, [Rn, #offset]! ;*(Rn + offset) = Rn; Rn = Rn + offset(地址回写) (2)、一次传送两个数据 LDRD (Load Register Double): 一次填充两个寄存器LDRD R4, R5, [R6, #offset] ;R4 = *(R6 + offset); R5 = *(R6 + offset + 4) STRD (Store Register Double):一次存储两个值到内存STRD R4, R5, [R6, #offset] ;*(R6 + offset) = R4; *(R6 + offset + 4) = R5 (3)、块数据存取 LDM (Load Mutiple): 将一块数据从寄存器中加载到内存中(reg list)。 STM (Store Multiple): 将块数据从内存中加载到寄存器。 LDM与STM块内存操作都有一个后缀,下方就是这四种条件,我们假设下方R0寄存器中存储的值是0(R0 = 6) IA (Increment After): 传输后再增加值, 如:LDMIA R0, {R1 - R3} ;R1 = 6, R2 = 7, R3 = 8 IB (Increment Befor): 传输前增加值 如:LDMIB R0, {R1 - R3} ;R1 = 7, R2 = 8, R3 = 9 DA (Decrement After):传输后减少值 如: LDMDA R0, {R1 - R3} ;R1 = 6, R2 = 5, R3 = 4 DB (Decrement Before):传输前减少值 如:LDMDB R0, {R1 - R3} ;R1 = 5, R2 = 4, R3 = 3 (4)、单一数据交换:SWP SWP命令用来交换寄存器与内存直接的值,下方是SWP的指令格式: SWP{条件}{B} Rd, Rm, [Rn] 上述命令表示将Rn中内存地址所指向内存中的数据加载到Rd中,然后将寄存器Rm中的值存储到该内存地址指向的区域中。如果Rd = Rm, 那么Rn指向的内存中的值就会与Rd进行交换。如果加上条件后缀的话,就说明在满足该条件时进行操作,后缀B则是操作低8位。 5、比较、分支与条件指令 分支与条件指令是编程中不可或缺的指令,在处理一些特定的业务逻辑时会经常使用到分支与条件指令。分支说白了就是跳转,而分支与条件结合使用就是当满足一定条件后进行特定的跳转。接下来,将总结一下ARM指令集中常用的分支指令与条件指令,更确切的说是条件后缀。 (1)、比较指令 在ARM指令集中使用到的比较指令有CMN、CMP、TEQ、TST。有一点需要注意的是CMN与CMP是算术指令,TEQ和TST属于逻辑指令。比较指令在执行后总是会设置标志位(N、Z、C、V), 因为条件后缀是根据被设置的标志位来判断比较结果是否满足条件的。下方会给出详细的条件后缀。比较命令后方也是可以添加条件后缀的。 CMN (Compare Negative) ---- 比较负值, CMN相同于CMP, 但他允许你对负值进行比较CMN R0, R1 ;Status = R0 - R1 CMP (Compare) ---- 之所以说CMP,CMN指令是算术指令,是因为他们讲操作数进行减法操作,并且设置相应的标志位,但是不记录计算结果。CMN与CMP进行的是算术减法操作,所以会影响C -- Carry标志。 CMP R0, R1 ;Status = R0 - R1 TEQ (Test Equivalence) ---- 测试等价,TEQ对操作数进行异或(EOR)逻辑操作,来判断两个操作数是否相同。因为TEQ做的是异或运算,所以不会影响Carry标志位。 TEQ R0, R1 ;Status = R0 EOR R1 TST (Test bits) ---- 测试位,使用TST命令来检查是否设置了特定的位。TST命中令其实是将两个操作数进行按位与(AND)操作,将结果存储在标志位中。可以使用TST来测试寄存器中某些位的特定值。 TST R0, R1 ;Status = R0 AND R1 (2)、分支指令 常用的分支指令是B、BL、BX这三个指令。 B Lable ;该指令表示将PC设置成Lable, 而PC就是指向下一条将要执行的指令,所以B Lable执行后,接下来就会跳转到Label出进行下一条命令的执行。 BL Label ; 执行该指令说明将LR设置成PC - 4, 然后再将PC设置成Lable。在执行BL Lable这条命令时,PC中存储的就是当前BL这条命令,而PC - 4就是上一条指令的地址,将PC - 4赋值给LR,也就是记录下跳转执行完指令后要返回的地址。如果BL在添加上一些条件,那么BL{条件}就可以进行循环了。 BX Rd ; 该指令说明将Rd赋值给PC, 然后切换指令集(如从ARM指令集切换到Thumb指令集)。 (3)、条件后缀 上述的分支指令与条件后缀结合才能发挥其强大的功能和作用,解析这部分介绍的是就是我们的条件后缀。条件后缀不能单独的使用,要和其他命令一块结合使用,然后根据条件的结果来做一些操作。下方是所有条件后缀,条件是否成立是根据NZCV这四个标志位来判断的,因为我们在对一些数值进行比较时,会设置相应的标志位。然后我们就可以使用这些标志位来判断条件是否成立。NZCV就是我们之前所提到的几个标志位,Z(是否为零), C(是否进位), N(是否为负), V(是否溢出)四种标准位来判断的。 EQ: Equal 等于,(Z = 1) NE: Not Equal 不等于 (Z = 0) CS: Carry Set 有进位 (C = 1) HS: (unsigned Higher Or Same) 同CS (C = 1) CC: (Carry Clear) 没有进位 (C = 0) LO: (unsigned Lower) 同CC (C = 0) MI: (Minus) 结果小于0 (N = 1) PL: (Plus) 结果大于等于0 (N = 0) VS: (oVerflow Set) 溢出 (V = 1) VC: (oVerflow Clear) 无溢出 (V = 0) HI : (unsigned Higher) 无符号比较,大于 (C = 1 & Z = 0) LS: (unsigned Lower or Same) 无符号比较,小于等于 (C = 0 & Z = 1) GE: (signed Greater than or Equal) 有符号比较,大于等于 (N = V) LT: (signed Less Than) 有符号比较,小于 (N != V) GT: (signed Greater Than) 有符号比较,大于 (Z = 0 & N = V) LE: (signed Less Than or Equal) 有符号比较,小于等于 (Z = 1 | N != V) AL: (Always) 无条件,默认值 NV: (Never) 从不执行 6. 移位操作(LSL、ASL、LSR、ASR、ROR、RRX) 移位操作在ARM指令集中不作为单独的命令使用,它在指令格式中是一个字段。接下来将会介绍一下各种移位操作。如果你之前学过“数字电路”这门课的话,那么你肯定对这些移位操作并不陌生。 (1)、LSL ---- 逻辑左移(Logical Shift Left)与 ASL ---- 算术左移 (Arithmetic Shift Left) 逻辑左移与算术左移的操作是一样的,都是将操作数向左移位,低位补零,移除的高位进行丢弃。接下来我们来看一个示例,根据这个示例来看一下LSL或者ASL的工作方式。 MOV R0, #5 MOV R1, R0, LSL #2 上述命令,就是将5存储到R0寄存器上(R0 = 5), 然后将R0逻辑左移2位后传送到R1寄存器中。十进制5的二进制数值是0101,进行逻辑左移2位就是0001_0100, 也就是十进制中的20。其实没逻辑左移1位就相当于原数值进行乘2操作,5逻辑左移2位其实就是5 x 2^2 = 20。下方是该操作的原理图 (2)、LSR ---- 逻辑右移(Logical Shift Right) 逻辑右移与逻辑左移是相对的,逻辑右移其实就是往右移位,左边补零。用法与LSL类似,在此就不做过多赘述了。 (3)、ASR ---- 算术右移(Arithmetic Shift Right) ASR与LSR类似,唯一不同的是,LSR的高位补零,而ASR的高位补符号位。符号位为1,那么就补1,符号位为0那么就补零。 (4)、ROR ---- 循环右移(Rotate Right) 循环右移,见名知意,就是循环着往右移动,右边移除的位往高位进行填补。 今天的博客就先到这吧,毕竟篇幅有限,上面的命令是一些基本命令。其他的浮点指令在此就不做过多赘述了,如ABS--绝对值、ACS--反余弦、ASN--反正弦等等,就留个读者自己去看吧。
如果你对iOS逆向工程有所了解,那么你对Tweak并不陌生。那么由Tweak我们又会引出Theos, 那么什么是Theos呢,简单一句话,Theos是一个越狱开发工具包,Theos是越狱开发工具的首先,因为其最大的特点就是简单。大道至简,Theos的下载安装、编译发布都比较简单,越狱开发中另一个常用的工具是iOSOpenDev,因为本篇的主题是Theos,所以对iOSOpenDev不做过多赘述。本篇博客的主题是Thoes的安装及其使用。 一、Theos的配置与安装 Theos的配置与安装算是比较简单的,按照官方给的步骤来操作,问题不大。Theos的官方文档地址“官方Wiki”,其中给出了如何安装和配置Theos, 本部分内容也是按照官方的Wiki来提供的,当然进行该部分操作时,要保证你本地已经安装了Homebrew, 可以使用brew命令来安装一些依赖包。brew其实类似于Linux中的yum或者apt-get,就是一个包管理工具。如果你本地没有安装brew,那么请求自行Google,从而完成对brew的安装。 1.安装dpkg sudo brew install dpkg dpkg是Theos依赖的工具之一,dpkg是Debian Packager的缩写。我们可以使用dpkg来制作deb,Theos开发的插件都将会以deb的格式进行发布的。所以我在安装Theos之前要安装dpkg, 当然此处我们使用强大的brew来完成dpkg的安装。安装的具体过程如下所示: 2.安装ldid sudo brew install ldid 在Theos开发插件中,iOS文件的签名是使用ldid工具来完成的,也就是说ldid取代了Xcode自带的Codesign。下方就是ldid的安装过程。 3.Theos安装 git clone --recursive https://github.com/theos/theos.git 因为我们的Theos一般是安装在/opt/目录下的,所以先cd到/opt目录下,然后从github上相关的地址clone下来即可,步骤如下(下方安装过程挺长,请耐心等待): 下载好Theos后,要修改一下文件的权限,如下命令 sudo chown $(id -u):$(id -g) theos 至此,Theos安装完毕,就可以开启你的Theos之旅了。 二、使用Theos创建、编译、安装使用工具 上面我们搭建好Theos的环境后,接下来就开始使用我们的Theos来做些事情了。接下来我们将要使用Theos来创建一个使用工具,并进行编译,编译后安装到我们的越狱手机上。接下来来看一下这一系列的步骤。 1.配置$THEOS export THEOS=theos文件所在路径 进入到我们要创建实用工具的目录中,使用export定义如下的环境变量,如下所示。下方命令比较简单,你可以这么理解,就是使用export定义了一个变量这个变量的名字是THEOS,该变量中存储的值是/opt/theos。后边这个路径就是上述我们安装theos的路径了,如果你要使用该路径的话,使用$THEOS代替即可。当然该变量只在当前终端中可用。如下所示。 2.新建工程 $THEOS/bin/nic.pl 接下来我们就要使用theos来创建我们的工程了,创建工程也是比较简单的,就是调用我们theos目录中bin下的nic.pl命令。具体执行如下所示。在执行nic.pl命令后,会让你选择新建工程的模板,目前theos中内置的是12套模板,当然你可以从网上下载其他的模板。当然我们此处创建的是application_modern类型的工程,所以我们就选2即可,当然,如果你想创建tweak,那么就选11即可,下方我们选择的是第二个模板。 在选择模板后,紧接着会让你做一系列的操作,这一些列的操作和Xcode新建iOS工程的步骤类似。 (1)输入你的工程的名字(Project Name,必选项),此处我们工程的名字是FirstTheosApplication。 (2)输入包名(Package Name),包名的命名规则一般是你们公司域名的倒写,然后后边加上你的工程名字,此处我就随便写了一个,就是下方的com.ludashi.firsttheosapplication。 (3)输入作者的名字(Author/Maintainer Name), 此处我们输入的是Mr.LuDashi (4)然后如数类名的前缀(Class name prefix), 此处我们输入的是CE。 经过上述配置后,我们的工程就创建好了。 下方是我们创建后的工程文件目录,当然packages文件夹是我们编译打包后才生成的文件,其中的deb就是我们的安装文件。可以将该安装包安装到我们的越狱手机上。 3.编译打包前的准备工作 export SDKVERSION=9.3 export THEOS_DEVICE_IP=ios_device_ip 接着我们要做一些编译打包前的准备工作,SDKVERSION是编译工程时所使用的SDK,因为本机Xcode中是9.3的SDK,所以我们知道的SDKVERSION是9.3。指定完编译所需的SDK后,我们需要指定打包后的文件所安装设备的IP地址,使用THEOS_DEVICE_IP来指定。下方的IP地址是一个越狱手机的IP地址。 在指定这个设备IP之前,你要保证你的越狱设备安装了OpenSSH,并且可以在Mac的终端上进行ssh登录。 4.进行编译 make 做好编译前的准备工作后,紧接着就是编译我们刚才创建的工程了。首先进入到我们的firsttheosapplication目录中,执行make命令进行编译。如下所示。 5.进行打包 make package 编译完成后,我们要讲项目进行打包,这样我们的越狱设备才能进行安装。下方是调用make package命令进行项目的打包。打包后会生成后缀名为deb的安装包。 6.安装 make install 将该安装包,安装到相应的越狱设备。因为上面我们已经配置了越狱设备的IP地址,并且保证该台越狱设备可以通过ssh进行连接,所以我们直接调用make install命令就可以进行项目的安装。在安装过程中会让你输入ssh登录设备的密码,输入后会显示安装成功的操作,如下所示。 7.安装后的效果 下方就是我们项目安装后的效果。打开Cydia,选择已安装Tab, 会看到我们刚才安装的FirstTheosApplication(实用工具),我们可以点进去进行查看,其中的一些信息大部分是我们刚才配置的信息。到此我们一个完整的流程就走完了。 三、Tweak创建、编译、打包与安装 接下来我们要创建Tweak类型的工程,步骤与上述过程大同小异。也是需要使用nic.pl来创建,使用make编译,使用make package打包,使用make install安装。接下来就来看一下这一过程。 1.最终效果 开门见山,下方就是我们要实现的效果。接下来我们就要使用Theos来创建Tweak工程,下方就是我们Tweak工程要做的事情。就是当你的iPhone锁屏开启后,给你弹一个框,这个弹框就是我们Tweak工程Hook的代码,下方就是我们最终实现的效果。 2.Tweak工程创建 下方就是我们Tweak工程的创建,与上述工程的创建类似,不过我们在此选择的是Tweak模板。如下所示,我们将该Tweak工程命名为LockScreenAlter,其他配置项使用默认值即可。然后进入到我们的LockScreenAlter工程目录中,主要有下方四个文件。 3.Makefile文件 该文件类似于配置文件,用来指定工程用到的文件、框架、库、使用的SDK等等,将整个编译、打包、安装的过程进行自动化。下方就是我们Makefile中的内容,下方红框中是创建完工程后默认的配置,上面红框中是后来我们添加的配置。这些项指定了编译、安装时所需的参数,使其自动化。因为我本地的Xcode中是iOS9.3的SDK,所以下方指定的SDKVESION是9.3。 4.Tweak.xm文件的编写 (1)写hook代码前的分析 分析这一步是至关重要的,因为这一步可以让你明白你的代码作用于何处。因为我们要在锁屏的页面进行弹框,所以我们要在相应的锁屏页面添加hook。下方就是我们的分析过程。经过浏览系统的头文件,我们从下方路径中找到了SBLockScreenManager.h这个文件,从文件名不难推测出该文件就是负责iPhone系统锁屏的文件,于是乎我们对其进行hook实现。 下方是上述头文件的内容,从内容我们更加坚信SBLockScreenManager类就是用来管理系统锁屏的,因为其中有个字段是用来表示是否已经锁屏的isUILocked。该头文件中还有一个类方法和一个对象方法。当然这个类方法明眼一看就是用来获取该类的单例的。而对象方法lockUIFromSource……应该是用来锁屏和解锁的。于是乎想要在锁屏中弹框就要在SBLockScreenManager类中的唯一的对象方法中进行操作了。 (2)hook代码的实现 下方就是在Tweak.xm中的所有代码。是Logos语法,使用起来是比较简单的。%hook与%end成对出现,%hook后方跟的是我们要修改的类名,此处我们要对SBLockScreenManager进行修改,类似于OC中的继承操作。%orig,用来执行修改函数的原始函数,此处可以看做是OC语法中的super,类似于调用父类的方法。下方代码就用到这些Logos语法就足以在锁屏出进行弹框了。 常用Logos语法简介: %hook 指定需要hook的类名,以%end结尾 %log 用来打印log的,将信息输入到syslog中,如%log((NSString *)@"ZeluLi") %orig 执行被hook函数的原始代码,类似于super.method功能 %group 该指令用于%hook的分组,%group后边跟的是组名,%group也是必须以%end结尾,其中可以包含多个%hook %init 该指令用来初始化某个%group,一个group只有被初始化后才可生效,init必须在hook中进行执行。 %ctor tweak的构造器,用来初始化,如果不显式定义,Theos就会自动生成一个%ctor,并在其中调用%init(_ungrouped). 如:%ctor { %init(_ungrouped)} %new 该指令用来给现有的class添加一个新的函数。与Runtime中的class_addMethod相同。 %c 该指令用来获取一个类的名称,类似于objc_getClass。 上述就先涉及这么多,更详细的请参加:http://iphonedevwiki.net/index.php/Logos 5. control文件 control文件中存储的内容记录了deb包管理系统所需的基本信息,会被打包进deb包里。下方就是control中内容,其中存储的就是一些包名、工程名、版本、作者等等,与打包安装后在Cydia中看到的信息相同。 6、进行编译、打包、安装 编译打包安装的过程与上一部分类型,在此就只展示一下过程,不做过多赘述了。 (1)使用make命令进行编译 (2)打包:make package (3)安装到手机: make install 7.从Cydia中进行查看 下方就是我们成功安装后在Cydia中查看的截图,安装成功后,当你锁屏时就会弹出一个Alter。
上篇博客《iOS逆向工程之KeyChain与Snoop-it》中已经提到了,App间的数据共享可以使用KeyChian来实现。本篇博客就实战一下呢。开门见山,本篇博客会封装一个登录用的SDK, 该登录SDK中包括登录、注册、忘记密码等功能,当然该SDK中包括这些功能的UI、数据验证等业务逻辑、网络请求、数据存储等等。当然此处的登录SDK是一个简化版的,真正的登录SDK比这个考虑的东西要多的多,如果在加上多个App进行登录账号的共享的话,那么考虑的东西就更为复杂了。 本篇博客就先封装一个LoginSDK, 让后将该SDK植入到两个App中(一个暂且叫做“App One”, 另一个暂且称为“App Two”)。当App One登录成功后,当你在打开App Tow进行登录时,我们封装的LoginSDK会从KeyChain中取出App One的账号进行登录。前提是这两个App设置了Keychain Share。废话少说,进入今天的主题。 一、功能总述 在博客开始的第一部分,我们先来看一下我们最终要实现的效果。下图中所表述的就是我们今天博客中要做的事情,下方的App One和App Two都植入了我们将要封装的LoginSDK, 两个App中都设置了Keychain Share。当App One通过我们的LoginSDK登录后,在启动App Two时,会去检索是否有账号以及在分享的Keychain中存储了,如果有的话,那么不会弹出“登录”界面,直接进行隐式登录。当然上述这些工作都是在我们的LoginSDK中进行做的事情。 本部分算是本篇博客的一个综述吧,从下方截图中,我们能清楚的看到上述的两个App中都植入了我们接下来要封装的SDK。LoginSDK.framework就是我们封装的登录静态库,其中提供了用户所调用的API。 下方这个截图中的内容就是用户所调用LoginSDK的API。因为我们做的只是一个Demo,所以下方的API接口比较简单,如果你要和现实App中真正的需求和业务逻辑整合到一块,那么封装一个登录用的SDK是非常麻烦的。因为我考虑过把我们团队所开发的几个App中的登录模块封装成SDK, 仔细考虑了一下,东西还是蛮多的。扯远了,不过今天这个Demo还是可以提供一个大体思路的。 下方API的对象是通过单例来获取的,如果是首次登录的话,就需要调用getLoginViewController这个方法来获取登录页面,并且这个函数需要提供一个Block参数,这个Block参数用来处理登录成功后的事件。而登录失败等事件就在我们SDK中自行处理了。 checkHaveLogin方法是用来检查是否已经有账号登录过,该方法需要提供两个Block,一个是登录成功要执行的Block,一个是没有已登录账号时执行的Block。当执行该方法时,如果之前有账号登录过的话,就直接进行隐式登录,登录成功后执行loginSuccessBlock。之前如果没有账号在此设备上登录就执行noAccountBlock, 来处理首次登录的事件。 该部分先聊这么多,接下来会根据上述的知识点详细的展开。 二、LoginSDK的封装 在封装LoginSDK之前呢,SDK的源代码以及所依赖的资源得准备好对吧。下方截图就是我们LoginSDK的源代码,下方绿框中的部分是留给用户使用的API, 而黄框中的部分就是我们这个SDK所依赖的资源了,虽然此处只用一个Storyboard,我们还是有必要将该资源文件打包成Bundle文件提供给用户的。而其他源代码SDK的用户是看不到的。源码准备好,测试完毕后,接下来我们就要进行SDK的封装了。 1.创建iOS Framework工程 首先我们需要创建一个iOS的CocoaTouch工程,点击Next,输入我们Framework的名字即可。下方我们暂且将该Framework的名字命名为“CreateLoginSDKFramework”。如下所示: 2.设定兼容版本 创建完工程后,我们要选择“Deployment Target”, 此处我们选择的是8.0。也就是说此处我们封装的SDK所支持的iOS系统版本是iOS8.0+。 3.选择“静态库” 我们创建的framework默认是动态库,所以我们要讲Mach-O Type设置为静态库“Static Library”,如下所示。 4.引入源代码并进行编译 配置好上述选项后,接下来我们就需要将我们事先准备好的SDK源代码引入到我们的Framework的工程中进行编译了,在编译之前我们要选择SDK用户可以看到的文件。下方截图中就是在Build Phases下的Headers中进行设置的。将用户可以看到的头文件房子Public中,用户看不到的放在Project中。如下所示。 5.编译 上述设置和配置完毕后,我们就要对我们的Framework工程进行编译了。先选择模拟器进行编译,然后选择真机进行编译。编译完后,在Products下会生成相应的Framework, 然后通过Show in Finder进行查看即可。查看时,如果想看“模拟器”和“真机”的framework的话,在Show in finder后,需要前往上层文件夹查看。具体如下所示。 6.Framework的合并 因为在模拟器下编译会生成模拟器下使用的Framework,在真机下编译会生成真机使用的Framework。如果想我们生成的Framework既可以在真机下使用,也可以在模拟器下使用,那么我们需要将两个Framework进行合并。 下方截图中,这两个framework一个是真机生成的,另一个是模拟器生成的,我们做的事情就是将下方绿框中的两个文件进行合并。然后使用合并后的文件将下方的文件替换即可。替换后的framework就可以在模拟器和真机下进行使用了。 我们使用“lipo -create 模拟器framework路径 真机framework路径 -output 新的文件”命令将上述两个文件进行合并。下方就是合并上述两个文件的执行命令, 执行完下方命令后会生成合并后的文件,将上述文件进行替换即可。经过上述步骤,我们的Framework至此就封装完毕了。 三、封装Bundle 封装完Framework后,接下来我们要对Framework依赖的资源文件进行打包了。因为我们SDK中的界面是使用Storyboard做的,所以需要将Storyboard打包成Bundle资源文件与上述的Framework一起使用。如果我们SDK中需要一些图片资源的话,也可以进行一并打包。接下来我们就要对资源文件进行打包。 1.Bundle工程的创建 首先我们像创建Framework工程一样创建一个Bundle工程,因为iOS工程下方没有Bundle类型的工程,所以我们需要在OS X -> Framework & Library -> Bundle下面来创建我们的Bundle工程。选择完后,输出我们的Bundle文件的名称即可,如下所示: 2. Bundle工程的配置 创建完Bundle工程后,我们要对其进行相应的配置。因为我们是选择OS X创建的Bundle,默认的Bundle是不能在iOS中使用的,所以我们得将Base SDK进行设置,选择相应的iOS版本即可,如下所示。选择完Base SDK后,我们还要像上面Framework的封装一样,设置一下要兼容的iOS版本(iOS Deployment Target), 在此就不做过多赘述了。 3.引入资源,进行编译 进行上述配置完后,接下来就是引入资源文件进行编译了,下方引入的资源文件就是我们的LoginSDK.storyboard。引入资源后,进行编译,编译后会在Products下面生成相应的Bundle资源文件,该文件就可以和我们的Framework进行使用了。 4.Bundle资源的加载 生成完Bundle资源文件后,我们在SDK的源代码中,要从Bundle资源文件中进行资源的加载。下方代码就是加载相应Bundle的代码。通过下方的宏定义,就可以通过“Bundle”的名字来加载Bundle。下方的LOGIN_SDK_BUNDLE就是我们要使用的Bundle资源文件的对象。 #define LOGIN_SDK_BUNDLE_NAME @"LoginSDKResource.bundle" #define LOGIN_SDK_BUNDLE_PATH [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: LOGIN_SDK_BUNDLE_NAME] #define LOGIN_SDK_BUNDLE [NSBundle bundleWithPath: LOGIN_SDK_BUNDLE_PATH] 下方代码就是从上述Bundle对象中加载相应的Storyboard。与我们之前的代码不同,之前我们是从MainBundle中加载的Storyboard,而现在我们是从指定的Bundle中来加载Storyboard。具体代码如下所示。 四、SDK的引入 SDK已经依赖的资源文件封装完毕后,接下来就是在其他App中使用了。在第一部分中的App One和App Two都引入了上述我们封装的LoginSDK。引入SDK步骤也是比较简单的,这和引入友盟,个推,微信支付,支付宝等等SDK的步骤差不多。下方就是我们引入SDK的步骤。 1.导入SDK并进行相关配置 导入SDK到我们的App工程后,我们要对其进行相应的配置。首先我们要对Framework Search Paths进行配置,也就是说告诉编译器我们的第三方SDK所在的位置。下方这个配置项在引入SDK后就默认存在的,如果没有的话就进行配置即可。 配置完路径后,接下来我们要在Other Linker Flags添加上-Objc和-all_load选项。这两个选项在之前的博客中也不止一次的提到过。-Objc这个flag告诉链接器把库中定义的Objective-C类和Category都加载进来。而-all_load会强制链接器把目标文件都加载进来,即使没有objc代码。根据上面介绍的,下方即使不添加-Objc这个选项,下方的工程也是可以正常运行的。 2.SDK的使用 配置完毕后,接下来就是在我们App中使用该SDK了。下方代码就是我们上述LoginSDK的使用方式,首先获取单例,然后检查是否登录,登录成功后根据Block回调跳转到首页,如果未登录,就通过LoginAPI获取登录页面进行登录。具体如下所示。 五、Keychain共享 关于Keychain共享的东西,我们可以看一下上一篇博客的介绍《iOS逆向工程之KeyChain与Snoop-it》。而在本篇博客中,是对keychain共享的应用,在植入上述LoginSDK后,如果想多个App间进行账号共享的话,要在相应的App上添加Keychain Share的标示了。下方截图就是我们第一部分那两个App中所设置的Keychain共享的配置项了。具体如下所示。 经过上面的所有步骤,我们封装了一个简单的LoginSDK, 并在多个App中进行植入,并且进行了账号共享。依照之前的风格,将本篇博客所涉及的所有内容都会在Github上进行分享,下方就是github分享地址。欢迎交流,上述内容有什么不足之处,欢迎批评指正,谢谢。 github分享地址:https://github.com/lizelu/LoginManagerSDKSimpleDemo
在前段时间呢陆陆续续的更新了一系列关于重构的文章。在重构我们既有的代码时,往往会用到设计模式。在之前重构系列的博客中,我们在重构时用到了“工厂模式”、“策略模式”、“状态模式”等。当然在重构时,有的地方没有点明使用的是那种设计模式。从今天开始,我们就围绕着设计模式这个主题来讨论一下我们常用的设计模式,当然“GoF”的23种设计模式不会全部涉及到,会介绍一些常见的设计模式。在接下来我们要分享的设计模式这个系列博客中,还是以Swift语言为主来实现每种设计模式的Demo。并且仍然会在GitHub上进行Demo的分享,希望与大家相互交流,相互学习,有不足之处还望批评指正。 今天博客的主要思路是先围绕着“穿越火线”中的角色与武器的关系,通过策略模式来设计实现这种关系,整体的来整体感受一下“策略模式”的优点。然后再参考《Head First Design Patterns》这本书中的鸭子的示例,来一步步使用Swift来实现策略模式的案例。当然我们只是参考《Head First Design Patterns》中的示例,本篇博客中的示例与其中的示例还是有所区别的。大部分设计模式的案例都是使用Java实现的,我们依然会使用Swift来实现。还是那句话,设计模式是针对面向对象编程语言的,而不是针对某一种编程语言,Swift是面向对象的语言,所以设计模式用于Swift编程中是没有问题的。废话少说,进入今天博客的主题。 一、穿越火线中的“策略模式”(Strategy Pattern) 当然,这个示例是我YY出来的示例,不是“穿越火线”这个游戏的设计方案呢。说到"穿越火线"如果你没有玩过,那应该听过吧,就是“CrossFire”。我平时不怎么玩游戏,穿越火线之前体验过,不过只有被爆头的份儿。听说那些游戏玩家现在不怎么玩儿“CF”啦,改玩儿Dota,LOL啦,真的是这样吗?我个人对于游戏而言是外行了,不过玩个超级玛丽、魂斗罗、植物大战僵尸、节奏大师还是可以的(坏笑)。 言归正传,今天我们就模拟穿越火线中角色和武器的关系,使用“策略模式”来实现。首先我们先分析一下这个场景,穿越火线中角色分为不同的等级,也就是“军衔”了,简单的说几个吧,由高到底对应着“军师旅团营连排小工兵”,上面的是组织,军衔莫过于各种级的士官,少中上尉,少中上校,少中上将(应该对吧,本人不太专业呢,不过用于咱们要实现的例子是够了)。我虽然不怎么会打CF,可是我会玩军棋呢。 我是不是刷知乎刷多了,不能在这儿“一本正经的胡说八道”了。言归正传,不同的角色所配备的武器装备也不同,等级越高所使用的武器装备也就越厉害。我们如何使用面向对象来表达这种角色与武器之间的关系呢?我们先看一下下方的类“类图”。 上面是一个简化的类“类图”,上面这种形式可以表达我们之前的那种场景。“军人”是一个父类,其他具体等级的军官都继承自“SuperClass”。那么问题来了,在上面那种模式下,如果只有“少尉”和“中尉”配备某种武器,其他军官不配备,我们就要在“少尉”和中尉的类中分别添加要实现的武器,那么这样会产生冗余的代码。还有个问题是上面的设计形式不利于扩展,比如“少尉”也要配备狙击步枪,岂不是得从“中尉”中的狙击步枪的方法复制到“少尉”中。这样也会产生重复代码的。那么我们该怎样去解决这个问题呢? 有童鞋说了,在Swift中的Protocol(协议,也就是Java中的接口)可以提供默认的实现。也就是声明一个protocol,然后通过extension来为协议添加默认实现,只要是类遵循该协议,那么这个类就拥有了这个默认实现(当然,Java中的接口是不能通过后期的延展来为其添加默认实现的)。如果在Swift中使用接口的默认实现的话,如果要对上述军官扩充装备的话,设计中的类“类图”(不是类图,但与类图相似)实现如下所示: 上面这种设计模式虽然不会产生重复的代码,但是如果给“军官”添加的武器过多的话,那么会导致相应的类中实现的接口过多,这并不是我们想要的。下方将会给出一个良好的解决方案,也就是使用策略模式。 二、使用“策略模式”(Strategy Pattern)对上述关系进行设计 “策略模式”的定义大概是:策略模式,将不同的策略(算法)进行封装,让他们之间可以相互的替换,此模式让策略的变化独立于使用策略的用户。在设计模式中有不同的设计原则,其中有一条就是“找出程序中可能需要变化的地方,并且把它吗独立出来,不要和不变的代码混在一起”。根据这条设计原则,然后结合着上述示例不难分析出来,在上述示例中,使用军官使用的不同武器是可以变化的,使用不同的武器正是采取不同的策略呢。 所以经过上述讨论,我们可以使用“策略模式”来重新设计上面的结构。简单的说就是把变化的“武器”部分进行提取,然后在军官中进行使用,不同的军官可以采取不同的策略,并且可以随时替换。下面是我们使用“策略模式”重新设计后的关系,具体请看下图。 在上面的类“类图”中我们对可变的“武器策略进行了提取”。我们使用了WeaponBehavior协议来规定武器的策略,使得不同的武器对外有统一的接口,在此就是使用武器,也就是开火。不同的武器使用不同的的“开火策略”,但是对外的接口都是一样的。设计原则中有一条是“面向接口编程,而不是面向实现编程”。这里所指的接口可以是协议,可以是抽象类,也可以是超类,其实就是利用面向对象的“多态”特性。上面的红框中实现的就是所有不同的策略。 而绿框中是我们的用户,也就是军官的定义,是我们不变的部分。在军官中也有一个基类,在基类中定义了军官的共性,其中依赖于“武器策略”的接口。在军官超类中使用“武器策略”的协议声明了一个对象,该对象就是该军官所采取的武器策略。在军官的超类中可以通过setWeapon()方法采取不同的策略,其中fire()方法就是使用该“武器策略”进行开火。在具体的军官中的changeXXX()方法就是调用setWeapon()方法进行策略切换的方法。具体内容请看下方的具体实现。 三、上述“策略模式”(Strategy Pattern)的具体实现 上面给出了“武器策略模式”的个个部分之间的关系,并给出了相应的解释。如果对此你感觉到抽象的话,那么我们接下来就用相应的Swift代码去实现上述示例。也就是将上面的理论部分进行具体实现,当然在此我们用的是Swift语言,但是,你完全可以使用其他的面向对象编程语言。下面就是我们具体的代码实现。 下方就是我们对“武器策略”的实现,红框中对应的就是上面图中的WeaponBehavior(协议)接口,下方绿框中就是不同武器的策略,每个武器策略都遵循了WeaponBehavior协议。并且实现了相应的useWeapon()方法。 对“武器策略”模块实现完毕后,接下来我们就得实现军官模块了。也是根据上面我们所画的“模式结构图”来实现我们的“军官模块”,下方Character就是所有军官的基类,其中默认的武器策略weapon就是手枪(PistolBehavior),其中有设置策略和改变策略的方法,并且还有使用策略的方法(fire())。下方的红框就是实现的不同的军官了,不同的军官可以有不同的切换策略的方法。具体如下所示: 上面就是我们全部实现的代码,下方是我们的测试用例和输出结果。下方我们创建了一个“中尉”军官----lieutenant,军官默认的是开的手枪。但是可以调用相应的changeXXX()方法来切换武器策略。开手枪时,发现火力不行,然后就调用changeHK()方法切换到HK48步枪。这种关系使用“策略模式”就比较灵活,并且便于扩展。比如中尉现在也要配备大狙,因为现在已经有大狙这个武器策略了,所以我们现在只需在中尉中添加相应的change方法,传入大狙的武器策略即可,具体的就不在演示了。
以下是学习git时常用的命令,大致总结了以下,用git做版本控制所用的命令挺多的,但常用的也在大脑承受的范围之中,把自己总结的东西给大家分享一下。 1.创建Git库:git的初始化用cd切换到要换的目录用“git-init”初始化(-代表空格) 2.git-add向Git库中添加文件,在调用了git-add才可以做commit操作 3.git-rm删除库中的文件 4.git-ls-files来查看当前的git库中有那些文件 5.git-status查看版本库状态(建议每次commit前要通过该命令确认库状态。以免误操作.) 最常见的误操作:修改了一个文件, 没有调用git-add通知git库该文件已经发生了变化就直接调用commit操作,从而导致该文件并没有真正的提交。如果这时如果开发者以为已经提交了该文件,就继续修改甚至删除这个文件,那么修改的内容就没有通过版本管理起来。如果每次在提交前,使用git-status查看一下,就可以发现这种错误。因此,如果调用了git-status命令,一定要格外注意那些提示为 “Changed but not updated:”的文件。这些文件都是与上次commit相比发生了变化,但是却没有通过git-add标识的文件。 向版本库提交变化git-commit.直接调用git-commit会提示填写注释。 1.可以通过git-commit -m"注释",必须得有注释,不然不能提交. 2.git-commit还有一个–a的参数,可以将那些没有通过git-add标识的变化一并强行提交,但是不建议使用这种方式。 3.每一次提交,git就会为全局代码建立一个唯一的commit标识代码,用户可以通过git-revert命令恢复到任意一次提交时的代码。 4.可以 用git-diff来查看具体那些文件发生了变化 5.在提交后还可以通过git-log,命令来查看提交记录 分支管理git-branch:当第一次执行git-init时,系统就会创建一个名为”master”的分支。而其它分支则通过手工创建。 1.创建一个属于自己的个人工作分支,以避免对主分支 master 造成太多的干扰,也方便与他人交流协作。 2.当进行高风险的工作时,创建一个试验性的分支,扔掉一个烂摊子总比收拾一个烂摊子好得多。 3.git-branch 查看已存在的分支 4.git-branch 分支名:创建分支 git-branch 分支名称 git-checkout -b分支名 使用第一种方法,虽然创建了分支,但是不会将当前工作分支切换到新创建的分支上,因此,还需要命令”git-checkout 分支名” 来切换, 而第二种方法不但创建了分支,还将当前工作分支切换到了该分支上。 1.删除分支git-branch -D git-branch –D 分支名可以删除分支,但是需要小心,删除后,发生在该分支的所有变化都无法恢复。 2.切换分支git-checkout 分支名 如果分支已经存在, 可以通过 git-checkout 分支名 来切换工作分支到该分支名 3.查看历史分支 git-show-branch * [dev1] d2 ! [master] m2 -- * [dev1] d2 * [dev1^] d1 * [dev1~2] d1 *+ [master] m2 在上述例子中, “--”之上的两行表示有两个分支dev1和master,且dev分支上最后一次提交的日志是“d2”,master分支上最后一次提交的日志是”m2”。 “--”之下的几行表示了分支演化的历史,其中 dev1表示发生在dev分支上的最后一次提交,dev^表示发生在dev分支上的倒数第二次提交。dev1~2表示发生在dev分支上的倒数第三次提交。 合并分支 - git-merge git-merge的用法为:git-merge “some memo” 合并的目标分支 合并的来源分支。如: 如果合并有冲突,git会由提示,当前,git-merge已经很少用了,用git-pull来替代了。 用法为:git-pull 合并的目标分支 合并的来源分支。 如git-pull . dev1 从远程获取一个git库git-clone 通过git-clone获取的远端git库,只包含了远端git库的当前工作分支。如果想获取其它分支信息,需要使用”git-branch–r” 来查看, 如果需要将远程的其它分支代码也获取过来,可以使用命令” git checkout -b本地分支名远程分支名”,其中,远程分支名为git-branch –r所列出的分支名, 一般是诸如“origin/分支名”的样子。如果本地分支名已经存在,则不需要“-b”参数。 从远程获取一个git分支 –git-pull git-pull username@ipaddr: 远端repository名远端分支名:本地分支名。这条命令将从远端git库的远端分支名获取到本地git库的一个本地分支中。其中,如果不写本地分支名,则默认pull到本地当前分支。需要注意的是,git-pull也可以用来合并分支。 和git-merge的作用相同。因此,如果你的本地分支已经有内容,则git-pull会合并这些文件,如果有冲突会报警。 将本地分支内容提交到远端分支 –git-push git-push和git-pull正好想反,是将本地某个分支的内容提交到远端某个分支上。git-push username@ipaddr: 远端repository名 本地分支名:远端分支名。这条命令将本地git库的一个本地分支push到远端git库的远端分支名中。 库的逆转与恢复 –git-reset 库的逆转与恢复除了用来进行一些废弃的研发代码的重置外,还有一个重要的作用。比如我们从远程clone了一个代码库,在本地开发后,准备提交回远程。但是本地代码库在开发时,有功能性的commit,也有出于备份目的的commit等等。总之,commit的日志中有大量无用log,我们并不想把这些 log在提交回远程时也提交到库中。 因此,就要用到git-reset。Git-reset的概念比较复杂。它的命令形式:git-reset [--mixed | --soft | --hard] [<commit-ish>] 命令的选项: --mixed 这个是默认的选项。 如git-reset [--mixed] dev1^(dev1^的定义可以参见2.6.5)。它的作用仅是重置分支状态到dev1^, 但是却不改变任何工作文件的内容。即,从dev1^到dev1的所有文件变化都保留了,但是dev1^到dev1之间的所有commit日志都被清除了,而且,发生变化的文件内容也没有通过git-add标识,如果您要重新commit,还需要对变化的文件做一次git-add。这样,commit后,就得到了一份非常干净的提交记录。 --soft 相当于做了git-reset –mixed,后,又对变化的文件做了git-add。如果用了该选项,就可以直接commit了。 --hard 这个命令就会导致所有信息的回退, 包括文件内容。 一般只有在重置废弃代码时,才用它。执行后,文件内容也无法恢复回来了。
下面是自己学HTML+DIV+CSS+JS时的学习笔记,给大家分享以下,相互学习。大二时候寒假在家无聊的时候想做点事,总结了一下web前端基础的东西,下面的每个字都是自己手敲的。 1、<html>和</html> 标签限定了文档的开始和结束点。 属性: (1) dir: 文本的显示方向,默认是从左向右 (2) lang: 表示整个文档中所使用的语言,en--英文,zh--中文 (3) version:定义创作文档的HTML的标准版本 2、<head></head>用于封装位于文档头部的其他标签 属性: (1) dir:文本的显示方向 (2) Lang:语言信息 (3) Profile:提供了与当前文件相关联的文档数据的URL 可放在<head>标签中的标签为 (1) <base>:标注当前文档的URL的全称 属性: Href:指定文档的基础URL地址(<body>中的相对地址都是以此基地址为基础) Target:定义打开页面的窗口 属性值: _parent:在上一级窗口中打开 _blank:在新一窗口中打开 _self:在本窗口中打开 _top:在浏览器的整个窗口中打开 (2) <basefont>:设定基准的字体,字号和颜色 属性: Face:设置字体(如黑体,楷体等) Size:设置大小(属性值从1——7,从小到大) Color;字体颜色(值为十六进制颜色) (3) <title>:设定显示在浏览器左上方的标题内容 属性: Dir:文本的显示方向 Lang:语言信息 (4) <meta>:有关文档本身的元素信息 属性: http-equiv: 生成http标题域,取值与content的属性值相同 属性值: Refresh 为自动刷新,在content里设定刷新时间,content里也可以跟上刷新的URL,实现页面跳转; content-type 在content里用charset设置内码语系 如charset=gb2312; Expires 定义网页有效期,在content里的格式为星期,日 月 年 时 分 秒 GMT,用英文和数字 Page-enter 进入网页时的效果 Page-exit 退出网页时的效果 在content中对应的值为: 0:盒状收缩 1:盒状展开 2:圆形收缩 3:圆形展开 4:向上擦除 5:向下擦除 6:向左擦除 7:向右擦除 8:垂直百叶窗 9:水平百叶窗 10:横向棋盘式 11:纵向棋盘式 12:溶解 13:左右向中部收缩 14:中部向左右展开 15:上下向总中部收缩 16:中部向上下展开 17:梯状左下展开 18:梯状左上展开 19:梯状右下展开 20:梯状右上展开 21:随机水平线 22:随机垂直线 23:随机 Name:如果元数据是以关键字/取值出现的话,那么name的值就是其关键字 属性值: Keywords 在content里定义关键字; Discription 为定意描述,在content里定义描述内容; Author 在content里描述作者; Content: 关键字/取值的内容 (5) <style>:设定有关CSS层叠样式表的内容 (6) <link>:设定外部文件的链接 (7) <script>:设定文件脚本的内容 3、<body></body>界定了文档的主题 属性: (1)、text: 页面文字的颜色 (2)、bgcolor: 页面背景的颜色(用十六进制的颜色表示) (3)、background: 页面的背景图像(所需的是图片的URL) (4)、bgproperties: 页面的背景图片是否固定(其只有一个值fixed,设为Fixed后图像不会随着滚动条的滚动而动) (5)、link: 页面默认的链接颜色 (6)、alink: 鼠标正在单击时的链接颜色 (7)、vlink: 访问过后的链接颜色 上面三个控制的是标签<a></a>中的颜色 (8)、topmargin: 页面的上边距 (9)、leftmargin: 页面的左边距 4、&nbsp;定义空格 <!--被注释掉的内容--> 5、文字标记 (1)、<hn>(n=1~6)标记标题字 属性: Dir:文字方向 Lang:语言信息 Align:对齐方式 属性值: Left:左对齐(默认) Right:右对齐 Center:居中 Class:用一个名称来标记标题,标记名称指向在外部定义的样式表 Id:为段落设置一个标记,将来可以在一个超链接中明确的引用这个标记,以便作为样式表的选择器 Style:创建标题内容的内联样式 Title:给标题加上一个说明性的文字 (2)、<font>标记普通字 属性: Face:字体 size: 字号 color:颜色 (3)、<b>或<strong> 粗体 (4)、<i>,<em>,<cite> 斜体 (5)、<sup> 上标 (6)、<sub> 下标 (7)、<big> 大字号 (8)、<small> 小字号 (9)、<u> 下划线 (10)、<s> 删除线 (11)、<address> 显示地址如Email 6、段落标记 (1)、<p> 表示一个段落的开始 属性:dir lang align class id style title (2)、<br> 换行 <nobr> 属性:class id style title (3)、<div>分块文字 属性:dir lang align class id style title nowrap(强制不换行) (4)、<span> 行内样式定义 属性: dir lang align class id style title (5)、<center> 水平居中显示 属性: dir lang class id style title (6)、<blockquote> 块引用 属性: dir lang class id style title 7、下划线 <hr> 插入水平分割线 属性:dir lang class id style align size noshade width color 8、列表 (1)、 <ul>无序列表,用<li>来罗列项目 属性:dir lang class id style title compact(紧凑无需列表)type(项目符号类型) Type的属性值:disc:实心原点 circle:空心原点 square:实心方形 (2)、<ol>定义一个有序列表 属性:dir lang class id style title compact start(数字起始值) Type的属性值:1,A,a,i,| (3)、目录列表<dir>,无序列表的一种特殊形式 属性: dir lang class id style title (4)、定义列表<dl> 基本用法: <dl> <dt>名词</dt> <dd>解释</dd> </dl> 属性:dir lang class id style title compact (5)、菜单列表<menu>,用于表示简短的列表 属性:dir lang class id style title 9、插入图片 <img> 插入图片标签 属性: Src:图像的源文件路径 Alt:文字提示(图像不显示时) width、hight:宽度、高度 border:边框 Vspace:垂直间距 hspace:水平间距 dynsrc:设定avi文件的播放 loop:设定avi播放次数 Loopdelay:设定avi播放延迟 start:设定avi文件的播放方 lowsrc:设定低分辨率图片 usemap:映像地图 Dir lang class id align style title Align的属性值极其说明: Top:文字的中线在图片的上方 middle:文字的中线位于图片的中部 Bottom:文字的中线位于图片的底部 left:图片在文字左侧 Right:图片在文字的右侧 absbottom:文字的底线位于图片的底部 Absmiddle:文字的底线位于图片的中部 baseline:英文文字基准线对齐 Texttop:英文文字上边线对齐 10、插入超链接 (1)、标签<a></a>为超链接标签 属性: Href:指定链接地址 name:给链接命名 target:指定链接打开窗口 accesskey:链接热键 Dir lang align class id style title charset rel:指定从原文档到目标文档的关系 Rev:指定从目标文档到源文档的关系 type tabindex:对新窗口中的对象重新排序 URL格式: http://进入万维网站点 ftp://进入文件传输服务器 news://启动新闻讨论组 telnet://启动telnet方式 Gopher://访问gopher服务器 mailto://启动邮件(href=”mailto://sdut@qq.com? Subject=给我来信”) (2)、书签链接,试用于页面太长时,避免翻页,格式如下 <a name=”name”> 文字 </a> <a href=”#name”> 文字链接 </a> (3)、空链接: <a href=”#”> 链接 </a> (4)、脚本链接:<a href = “javascript:.....”> 文字链接 </a> (5)、制作图像映射:在同一图像上嵌入不同的链接,创建图像映射的方式是通过<img>标签的usemap属性再结合<map> 以及<area>标签来实现的,<a> 或<area>标签包含在<map>标签内 <map>的属性: name 给链接命名 dir lang id class style title <area>的属性: Href alt accesskey target dir lang id class style title tabindex shape(图像映射区域的形状) coords(图像对光标敏感区域的坐标) Shape的属性值: Rect 矩形区域 circle 椭圆形区域 poly 多边形区域 事例: <img usemap=”#map” src=”URL” hight=”” width = “” border = “”> <map name = “map”> <area shape = “rect” coords = “100,23,56,90” href = “URL”> <area shape = ...............................................................................> </map> 11、插入多媒体 (1)、插入声音标签<bgsound> 属性: src (声音文件路径) loop 循环次数 (2)、<embed>标签可以在网页中加入MP3音乐,电影,swf动画等多媒体文件 属性: src loop autostart width hight hidden(是否隐藏内嵌播放器) dir lang class id style align title type(嵌入多媒体类型) Hidden 和 autostart 的属性值有true和no 当嵌入flash动画时还有以下属性: Quality 动画的播放质量 puginspage 播放插件所在位置 wmode 动画播放时的窗口模式 (3)、制作滚动字幕标签<marquee></marquee> 属性: align behavior(滚动方式) bgcolor class direction(滚动方向) width hight Hspace vspace Style loop scrollamount(滚动速度) scrolldelay(滚动延迟) Direction的属性值:up down left right Behavior 的属性值: scroll(循环往复《默认》) slide(只走一次滚动) alternate(交替进行滚动) Scrollamount后跟数字越小越慢,scrolldelay后跟毫秒,vspace和hspace 定义字幕与周围文字的距离 12、其他嵌入 (1)、<object>标签用于往文档中嵌入对象 属性:classid(指定包含对象的位置) archive(URL列表) border codebase(提供一个可选的插件URL)hight Width data(指定需要对象处理的数据文件) hspace vspace name type tabindex dir lang align class id style title (2)、<applet>标签用来插入applet小程序 属性: code (指定浏览器运行的Java类小程序的名称) codebase hight width hspace vspace name type class alt id title style align (3)、<param>标签为把包含他的<object>或<applet>提供参数 属性:type name id value 13、表单: 表单由一个或多个文本输入框、可单击的按钮、多选框、下拉菜单和图像按钮等组成,所有的这些都放在<form>中 (1)、<form> 属性: dir lang align class id style title name method(定义表单结果从浏览器传输到服务器的方法一般有post 和get 两种方法) action(用来定义表单处理程序(asp,CGI等)的位置(相对或绝对)) enctype(设置表单编码方式) target(表单返回显示方式) Enctype的属性值: text/plain(以纯文本形式传送) application/x-www-urlencoded(默认编码方式) multipart/form-data 使用mine 编码 (2)、表单输入标签<input> 属性: dir lang class id alt name align style title type accesskey value size src accept(文件上传的MIME表列) checked(已选中) disabled(禁止某个元素输入)maxlength(最大字符数) Type的属性值:text 文字域 password密码域 file 文件域 checkbox复选框 radio单选框 button 普通按钮 submit 提交按钮 reset 重置按钮 hidden隐藏域 image 图像提交按钮 (3)、多行文字域<textarea> 属性:dir lang class id style title name accesskey disabled tabindex rows(行数) cols(列数) wrap(多行文字域的换行) Wrap的属性值:virtual 虚拟换行 physical 物理换行 off 不换行 (4)、<select>下拉菜单和下拉列表标签,把标记条目放在<option>标签中 属性:dir lang class id style title name disabled(禁用某个列表) size tabindex multiple(列表中的多选项目) 14、表格 (1)、定义表格<table> 属性:dir lang class id style title name bgcolor background bordercolor bordercolorlight bordercolordark border height width cellpadding(单元格边距) cellspacing(单元格间距) nowrap frame(表格边框的可见方式) rules(行列之间边的可见方式) summary(整个表格的概要描述) Frame的属性值: Above 显示上边框 below显示下边框 border 边框全显示 hside显示上下边框 vside显示左右边框 lhs显示右边框 rhs显示左边框 void 显示 Rules的属性值: All 显示所有内部边框 cols仅显示列边框 groups显示位于行列间的边框 none不显示内部边框 rows仅显示行边框 (2)、定义行<tr> 属性:dir lang class id style title bgcolor background bordercolorlight bordercolordark valign(表格行的垂直对齐方式) (3)、定义列<td>,<th>为定义表头 属性:dir lang class id style title bgcolor background bordercolorlight bordercolordark valign width height abbr(单元格的缩写) axis(用逗号分割目录名列表) rowspan(单元格跨行个数) Colspan(单元格跨列个数) headers(标题单元格的列表) nowrap(禁止换行) scope(指定单元格提供信息) (4)、表格标题内容<caption> 属性:dir lang class align id style title valign 15、框架主要分为两部分:一个是框架集,另一个是框架 (1)、<frameset>框架集,仅是一个框架的集合 属性:class id style title rows cols bordercolor frameborder framespacing(框架集间距) (2)、定义框架<frame> 属性:class id style title bordercolor frameborder name noresize(禁止调整边框大小) src(框架源文件) Marginwidth(框架边缘宽度) marginheight(框架边缘高度) Frameborder的属性值:yes 出现边框 no 不出现边框 (3)、<iframe>定义内联框架,在文档中定义一个独立的矩形区域,有独立的滚动条和边框 属性:class id style title frameborder name src marginwidth marginheight align height width scrolling(是否允许出现滚动条) Scrolling的属性值:yes 出现 no不出现 auto自动出现滚动条 16、样式表 (1)、内联样式表:只需在标签内含一个上style属性,style属性后在跟一系列属性和属性值即可。 事例:<标签 style = “属性:属性值”> (2)、文档样式表用<style>标签表示 属性:dir lang title media(文档要使用的媒介类型) type(样式类型) 级联样式表type的属性值都是text/css,javascript使用的样式表都是text/javascript Media的属性值: screen 计算机显示屏(默认) tv(电视) projection 剧场 handheld(PDA和手提电话) print 打印 all所有媒体 (3)、外部样式表:用<link>标签来实现 属性:dir title lang target type class id style charset href media rel rev (4)/样式表语法 (a)、HTML重新定义标签样式表语法: exp: td{color:red;font-size:8pt} (b)、类样式表:能够在文档样式表或外部样式表中为同一个元素创建不同的样式,在文档后面通过设置class属性来选择特定的样式。 例子: .bg {background-image: url(路径);} <body class = ”bg”> 17、样式表的属性 (1)、字体属性: font-family 用一个指定的字体名 font-size 字体显示的大小 font-style 字体显示的样式 font-weight 定义字体的粗细 font-variant 设置英文大小写转换 font 组合设置字体属性 Font-style的属性值:normal 正常值 italic 斜体 oblique 扁斜体 Font-weight的属性值: normal 正常值 bold 粗体 bolder 在加粗 lighter 变细 100—900 共有100到900个级别数越大越粗 Font-variant的属性值:normal 正常 small-caps 将小写转换为大写 Font组合时的顺序:样式,粗细,大小 (2)、颜色和背景属性: Color 颜色 background-color 背景颜色 background-image 背景图片 background-repeat 背景图片如何重复 Background-position 设置背景图片水平和垂直的位置 background 组合设置背景属性 属性值: Background-repeat:repeat 平铺 repeat-x X方向上平铺 repeat-y Y方向上平铺 no-repeat不平铺 Background-position: value 以百分比(x%.y%)或绝对值的形式(x.y)来确定背景图像的位置 top 居顶 Center 居中 bottom居底 left 居左 right居右 Background可以任意组合以上的属性值 (3)、文本属性: Letter-spacing 定义一个附加在字符间的间隔数量 word-spacing单词间的间隔数量 text-index文字的首行缩进 Text-align 文本对齐方式 line-height行高间隔 text-transform控制英文文字大小写 text-decoration文字修饰 属性值: Letter-spacing: normal 正常值 长度单位 如2em Word-spacing : normal 正常值 长度单位 Text-decoration:underline 加下划线 overline 加上划线 line-through 加删除线 blink闪烁文字,只使用于netscape浏览器 none默认值 Text-align:left 左对齐 right右对齐 center居中 justify两端对齐 Text-index:后跟长度单位如2em Text-transform: capitalize将每个单词首字母大写 uppercase 将每个都转换为大写 lowercase 转换为小写 none (4)、边框属性: Border-color 边框颜色 border-style 边框样式 border-width边框宽度 border-top-color 上边框颜色 Border-left-color 左边框颜色 border-right-color 右边框颜色 border-bottom-color 底边框颜色 border-top-style border-left-style border-right-style border-bottom-style border-top-width border-left-width Border-right-width border-bottom-width border 组合设置 border-top(right/left/right/bottom) 属性值: Border-style:none 无边框 dotted 边框由点组成 dash 边框由短线组成 solid边框是视线 double双线 Groove 立体沟槽 ridge 边框成脊形 inset 边框内嵌一个立体边框 outset边框外嵌一个立体边框 (5)、方框属性: Float 让文字环绕在元素四周 clear指定在某一元素的某一边是否允许有环绕的文字和对象 clip限定只显示裁切 出来的区域 width设定对象的宽度 height设定对象的高度 padding设定边框和内容间的距离 margin 元素里 浏览器的距离 overflow 当本层内容容纳不下时的处理方式 position 设置对象位置 z-index决定层的先后顺序和覆盖关系 属性值: Float: none left right Overflow: visible无论层的大小,内容都会显示出来 hidden 隐藏超出层的内容 scroll 不管是否超出都会添加滚动条 auto只有超出时才会有滚动条 (6)、列表属性: List-style-type 设定引导列表的项目类型 list-style-image 选择图像作为项目的引导符号 list-style-position 决定列表项目所缩进的程度 属性值: List-style-type: disc 在文本行前加实心圆 circle 加空心圆 square 加实心方块 decimal 加阿拉伯数字 Lower-roman 小写罗马数字 upper-roman 大写罗马数字 lower-alpha 小写字母 upper-alpha 大写字母 none 不显示任何符号 List-style-image:的属性值为URL(图片路径) List-style-position: outside 列表贴近左侧边框 inside 列表缩进 (7)、滤镜属性:基本语法 filter: 滤镜 (参数) Alpha 透明的层次效果 blur 快速移动的模糊效果 chroma 特定颜色的透明效果 dropshadow阴影效果 Fliph 水平翻转效果 flipv 垂直翻转效果 glow 边缘光晕效果 gray灰度效果 invert 颜色亮度值翻转 Mask遮罩效果 shadow渐变阴影效果 wave波浪效果 xray X射线效果 (8)、鼠标滤镜:用法---------cursor: value Value的值: hand 手型 crosshair 交叉十字 text 文本选择符号 wait沙漏装 default 默认形状 help 问号 N(W、S、E)-resize 向北(西、南、东)的箭头 18、在页面中加入Javascript脚本 (1)、用标签<script>实现 属性: charset编码脚本程序的字符集 language 脚本语言 src 包含脚本程序的URL type脚本类型 (2)、js内在事件:onBlur光标离开文本框时 onChange 当文本框的内容给被改变是时 onClick单击时 onLoad载 入时 onMouseOver鼠标经过时 onMouseOut鼠标移开时 onReset 复位表单时 onSubmit提交表单时 onSlecte 文本域被选中时 onUnload退出载入时 onFocus当光标落在文本框时
从今天就开始陆陆续续的发布一些有关Swift语言的东西,虽然目前在公司项目开发中Objective-C还是iOS开发的主力军,但是在不久的将来Swift将会成为iOS开发中的新生宠儿。所以在在Xcode6.0+版本的Playground上玩一玩Swift还是很有必要的。在接下来发表的博客中主要是总结一下自己在翻译《Swift编程入门经典》(清华大学出版社出版中)这本书所学到的东西。在翻译这本书的时候,自己是一名译者,但更是一名读者,拜读原著之后感觉学了不少新的东西,让自己的思维更为开阔。 学习一门新的编程语言,Code是少不了的。在翻译的时候,自己也是在不停的Coding,把书上的每段代码都敲了一遍。学编程,动手是关键,避免眼高手地的情况。在写代码时不要按部就班的去Coding,要学会思考,学会举一反三,学会自我的扩充。举个例子,在一个Demo中,你可以去尝试修改一些东西,然后预测一下将会得到什么样的结果,然后去验证是否和自己的预知一致,若不一致就要寻找原因了。其实在问题中成长是最快的,每个问题的解决就是自我提高的一个过程。有些事儿需要天赋,但有些事儿还是需要努力的,谁生下来就是技术大牛呢~大牛大部分不都是从吃白菜长大的么?好了不扯淡了,“少壮不努力,长大学编程”,切入今天的正题,来窥探一下Swift语言。 先说明一下编译环境:Xcode6.3,当然Xcode6.0以后的版本都支持Swift语言,然后我们就可以在苹果专门为Swift设计的Playground上来为所欲为的搞搞Swift语言了。 一、创建属于你的Swift工程。 首先第创建一个SwiftDemo的工程对吧,步骤如下: 1.如何打开Xcode我就不扯了,首先创建一个新的工程,如下: 2.然后选择iOS->Application->Single View Application点击next 3.键入ProductName :SwiftDemo,Organiztion Name, Organization identifier一般为你所在公司域名倒序如com.cnblogs.lizelu, 开发语言选择Swift,设备这儿我们选择iPhone。然后点击Next, 如果在你的项目中要使用CoreData来存储数据的话,你可以选择User Core Data。 4. 下面就是我们创建好的工程,Swift语言文件的后缀名是swift,它不在像Objective-C中的又.h和.m文件 二.如何在上面的创建的工程上实现HelloWorld。 1.当然了,有Storyboard干嘛不用呢对吧,在ViewController上添加一个Label: HelloWorld 并设置样式,然后在label上添加一个Button,并添加相应的约束,如下所示。 2.给我们的Button关联相应的点击事件,Connection选择Action, 然后在取个名(tapHelloWorldButton), 类型选为UIButton, Event选为 Touch Up Inside, 点击Connect。到这一步我们就为Button关联了点击事件了。 3.接下来要实现点击按钮后要做的事情:点击Button出来一个Alter, 该提示框的名字为hello, 上面有一个名为OK的按钮,上面的信息是“Hello Swift World”。 4.接下来激动人心的时刻到了,我们来运行一下我们的SwiftDemo工程(你可以真机调试,也可以使用模拟器调试),下面是运行出来的效果,点击Hello World!会弹出我们定义好的提示框啦。 三,总结 到此为止,你已经学会如何去创建一个Swift语言的工程,并且如何去实现Hello World的UI,以及如何给Button关联相应的事件. 现在你已经叩响了迈往Swift世界的大门,在接下来的日子中就让我们在Swift的世界中去畅游吧,如果对于iOS开发有问题可以在下面留言进行交流学习,或者加入上面的QQ交流群,共同进步与提高。一起期待下面的一些Swift教程吧,最后要感谢清华大学出版社给予的对《Swift编程入门经典》的翻译工作。
自从苹果公司发布Swift的时候,Xcode上又多了一样新的东西---"Playground"。Playground就像操场一样,可以供我们在代码的世界里尽情的玩耍,在本篇博客中就介绍如何借助Playground来快速的上手Swift编程。Playground在学习Swift语言中确实扮演着重要的角色,还是那句话,咸蛋就到这儿吧,下面就切入今天的正题,如何去创建一个Playground,又如何来使用Playground. 一.创建属于你的Playground 还是用之前的SwiftDemo演示来如何创建一个属于你自己的Playground. 1.在相应的文件夹下,右键点击,选择New File…………如下图所示: 2.经过第一步以后你会看到下面的界面,我们选择iOS中的Source下面的Playground,然后点击Next 3.键入你所创建的playground的名字,如下所示 4.创建好的Playground如下所示 二、在Playground中进行玩耍 1.Playground是所见即所得的,就是你在左边写的代码,会立刻在右边看到该行代码运行的结果,如下图所示,下面写了一个循环5次的for-in循环,右边就立刻出现了该循环对应的循环次数:5 times 2.你可以点击5 times后面的小白点对该循环进行更为详细的查看,如下图所示,点击白点后回出现该循环运行后在控制台上输出的内容,然后是每次循环index值的变化,是不是觉得特别的赞呢。 三、总结 今天的博客演示了如何在你的工程中创建Playground文件,并且如何去利用它来窥探Swift语言,接下来的博客中会把Playground作为向导来更为详细的窥探。Swift语言的更多的细节,如果对于iOS开发有问题可以在下面留言进行交流学习,或者加入上面的QQ交流群,共同进步与提高。一起期待下面的一些Swift教程吧,最后要感谢清华出版社给予的对《Swift编程入门经典》的翻译工作。
在上一篇博客“窥探Swift编程之在Playground上尽情的玩耍”中介绍了如何使用Playground来学习Swift语言。本篇博客就使用Playground来窥探Swift语言。千里之行始于足下,当然了,除非你是坐的高铁或者飞机。还是那句话从基础开始吧,本篇博客主要介绍一下Swift的基本数据类型。Swift中的数据类型可谓是百花齐放百家争鸣,下面就来窥探一下Swift中的基本数据类型。 一、Swift中的变量和常量 1.关键字“let” 常量,顾名思义,常量的值是不可以被再次改变的,比如你给number赋一个初始值"swift",那么number从你给它赋值的那一刻就代表着"swift"。就像你从生下来就是男的或者女的,你不能从一个真正的男的变成真正的女的,反过来也是不成立的。当然后天如果你去了趟泰国那就不算了,如果后天变性的,编译器会报错,说你这是人妖,是编译不过去的。说着说着又跑偏了,言归正传,在Swift中使用let关键字来创建一个常量并赋初始值后,那么该常量的值就不能被改变了,如果你在使用中尝试着去改变它,那么编译器真的会报错的。在OC和C++中可以用关键const来定义常量,而Swift中使用的是let。 下面的代码段定义一个名为gender的常量,并且赋值为"我是男的",在Playground的右边会紧接着出来gender的值,下面一句是尝试着给gender赋值,说你要去泰国那啥~在Swift的世界里是不允许你这么做的,所以给你一个错误“Cannot assign to 'let' value 'gender'”,就是不允许你给gender再次赋值,用一句通俗易懂的话就是,不允许你去泰国那啥。这就是Swift中的常量,一锤定音,一言既出,驷马难追是let的风格。 2.关键字“var” 如果世界上什么东西都是一成不变的,估计就生活不下去了对吧。不想当程序员的医生不是好老板,这些都是可以变得对吧,世界上一成不变的东西还是少的,大多数东西都在运动,都在变化,在Swift的世界里也是如此,变量的用处比常量要多的多。比如你现在是程序员,明天也要当老板,如果这东西是常量,那完了,就做一辈子程序员吧,但是在现实生活中是可以变得,下面就是使用关键字var来定义一个hopeMe变量,并且赋上“我是程序员”,你励志要做老板,有一天你做了老板,然后就变成了下面那句代码,这在Swift的世界中是可以的,所以编译器是不会给error的。 3.关于数据类型的说明 细心的园友会注意到上面不管是定义常量或者变量怎么没给这些变量或者常量指定数据类型呢?难道Swift像JS或者PHP这些语言一样是弱类型的?非也,并不是弱类型的,Swift可以自动推断数据类型,上面定义的都是string类型。下面这个实例,定义了一个变量intNumber,并赋上初始值10,使用sizeofValue()函数来查看该变量所占内存字节数,使用sizeof()函数来查看相应的数据类型所需存储空间。更好的方法是在键入该变量时查看提示的数据类型。 二、给变量显式指定相应的数据类型 上面不管是声明常量还是变量,都是有编译器通过类型推断来判断数据的类型的,接下我们就要显式的指定我们需要的数据类型。在Swift中数据类型可谓是丰富多彩呢。 1.显式声明不同字节的整数类型,在Swift中允许你来指定数据存储所需字节数。当然Int64是在64位系统上使用才占64个字节呢。 2.举一个简单的生活中的例子:不同物种是不可能进行结合产生正常的后代的,如果你告诉编译器马+Lv=?编译器会报个错,说你这是非法的,会产生一个不正常的后代“ErrorMessage = Luo子”。不扯咸蛋了,言归正传,在Swift中是不允许不同的数据类型进行隐式运算的。不同数据类型的数字是不允许进行算术运算的,即使同为整型,如果存储所占字节数不同,在Swift中也是不允许对其进行算术运算的。Swift语言说的官方一些是类型安全的,不同类型的数据是不允许直接进行算术运算的,下面的实例很好说明了这个问题。 3.Swift对不同数制的也是有着很好的支持,为了可读性,Swift中允许你对较大的数进行分隔如下图所示,0b表示二进制,0o表示8进制,0x表示十六进制。 三、Swift中的浮点型 1.其他编程语言中也是有浮点型的了,下面的Dmeo给出了不同类型的浮点型在内存中存储时所需字节数。在Swift中默认是Double类型。 2.下面的Demo中给出类浮点型的计算,Float类型的数据是不允许和Double类型的数据直接进行运算的,还是那句话:swift中是不允许进行隐式类型转换的。 3.下面是浮点数的指数表示形式,如下所示
今天的博客中就总结一下关于Swift中相对Objc新添加的两个数据类型:元组(Tuple)和可选值类型(Optional)。上面这两个类型是Swift独有的类型,使用起来也是非常方便的,今天就通过一些示例来介绍一下如何初始化和使用元组和可选值类型以及使用元组和可选值类型的场景。废话少说,直奔主题,在今天的示例代码中仍然是使用Playground来测试的。左边是代码,右边是Playground中的结果。 一、Swift中的元组(Tuple) 元组类似于C语言中的结构体(Struct),用来存储一组相关的值,并且允许这些相关值的类型不同。元组一般在数组返回多个值的时候使用。 1. 直接定义元组,不指定元组中元素的类型。定义元组的语法是直接使用一对小括号,括号中是由逗号分开的各个值。具体如下所示: 2.取出元组中的值,方式一:直接赋值给相应的常量(或者变量),下面这行代码就是直接把元组赋值给常量,在Swift中字符串之间的连接可以使用+号运算符。经过下面的语句就会把元组的值一次赋值给我们定义的常量。一句话概括:"一个萝卜一个坑,对号入座"。 3.还可以通过元组的下标索引来获取元素的值,元组的下标是从零开始的,下面是通过元组的下标来获取元组的元素值的。 4.我们还可以显式的指定元组的每个元素的类型,下面就为元组student1每个元素指定了数据类型,前两个是字符串类型,后两个是整型。如果你尝试着给字符串类型的元素赋值整型数据,则编译器会报错,同时会提示元组每个元素指定的数据类型。 5.给元组的每个元素赋上正确的值,就会在Playground上显示出每个元素应有的值。 二、Swift中可选类型(Optional) 可选类型是Swift中新的类型,可选类型使得Swift的数据类型更为安全,可选类型可以说在Swift中扮演着重要的角色,它使得你的代码更为健壮,下面将会给出可选类型的使用场景。可选值,用大白话解释就是不确定这个变量或者常量中是否有值(有可能为nil),所以这个变量或常量的值是可选的。下面对optional进行详细介绍。 1.可选值类型是可以直接赋值nil,为了类型安全而普通的变量和常量是不可以直接赋值nil的如下所示: 2. 定义可选值类型的语法是在类型声明后添加一个?号,说明这个变量的值是不确定的,所以喽就添加了一个?号。下面就是给可选值类型赋一个nil, 是不会报错的。 3.给可选类型变量赋一个值后,如果要使用这个可选变量的值,需要使用感叹号(!)强制打开,如果不使用感叹号(!)强制打开,编译器会报错。为什么要用感叹号强制打开可选变量的值呢? 原因很简单,因为可选类型的值有可能为nil, 使用感叹号(!)强制打开的原因是告诉编译器"这个可选变量的值不为空,我要使用它”,强制打开后,在使用可选变量的值时,编译器就不会报错了。 下面optionalValue是可选类型的变量,并且把“ludashi”赋值给它,如果不使用!强制打开optionalValue的值的话,编译会抛红报错。 4.来看一下可选类型的使用场景以及使用小实例。 (1)把字符串转换成整数时我们要用可选类型的变量(或者常量)来接受,因为我们不确定是否转换成功了,然后经过非空判断后,再使用感叹号(!)强制打开进行使用。 下面把一个数字字符串转换成整数,然后使用一个变量去接收,然后去判断这个变量是否为空,不为空就强制打开进行使用,下图转换后不为nil就会被强制打开然后使用。 下面是转换后为nil的情况, 就不会做处理,因为判断为nil, 所以就不做处理。 (2)当你使用某个类的某个方法时,你不确定该类是否有这个方法,可以使用?来打开该可选值,然后再调用该类的方法。?号就是告诉编译器你不知道该类是否有该调用的方法,如果有就调用,如果没有就忽略。如下实例所示: 如果numberInteger为nil的话, 就不会调用isEmpty, resultEmpty就会被赋值成nil,如下所示。 关于元组和可选类型的使用还得在项目中结合具体的应用场景来使其发挥更大的作用,本篇博客就先通过一些小的实例来看一下元组和可选类型的使用方法和使用场景,举一反三才是最重要的。今天的博客就到这吧。
想必写过程序的童鞋对枚举类型并不陌生吧,使用枚举类型的好处是多多的,在这儿就不做过多的赘述了。Fundation框架和UIKit中的枚举更是数不胜数,枚举可以使你的代码更易阅读并且可以提高可维护性。在Swift语言中的枚举可谓是让人眼前一亮。在Swift中的枚举不仅保留了大部分编程语言中枚举的特性,同时还添加了一些好用而且实用的新特性,在本篇博客中将领略一些Swift中枚举类型的魅力。 有小伙伴会问,不就是枚举么,有什么好说的。在Swift中的枚举怎不然,Swift中的枚举的功能要强大的多,不仅可以给枚举元素绑定值,而且可以给枚举元素关联多个值,同时还可以通过元素的值给一个枚举变量赋一个枚举值,并且枚举中可以定义枚举函数。下面将要开始窥探一下Swift中的枚举。 一、定义枚举 在Swift中枚举的定义与其他编程语言中定义枚举不同,在每个枚举元素前面多了一个关键字case, case后紧跟的是枚举元素,下面是定义枚举类型的两种方式。 1. 多个case, 每个枚举元素前都有个case //枚举的定义 enum KindOfAnimal { case Cat case Dog case Cow case Duck case Sheep } 2.一个case搞定所有元素,枚举元素之间使用逗号隔开 1 //你也可以这样定义枚举类型 2 enum KindOfAnimalTwo { 3 case Cat, Dog, Cow, Duck, Sheep 4 } 二、枚举类型的使用 定义完枚举类型就是为了使用的对吧,直接使用枚举类型声明变量即可, 在Swift中是不需要typedef来定义枚举类型的,我们可以直接使用枚举类型。 //定义枚举变量并赋值 var animal1: KindOfAnimal = KindOfAnimal.Cat 给枚举变量赋值时也可以把枚举类型名省略掉,因为在声明枚举变量时就已经指定了枚举变量的类型。var animal2: KindOfAnimal = .Dog 在Switch中使用我们的枚举变量 //在Switch…Case中使用枚举 switch animal1 { case KindOfAnimal.Cat: println("Cat") case KindOfAnimal.Dog: println("Dog") case KindOfAnimal.Cow: println("Cow") case KindOfAnimal.Duck: println("Duck") case KindOfAnimal.Sheep: println("Sheep") default: println("error = 呵呵") } 三、给枚举成员赋值 在Swift中声明枚举时,是可以给每个枚举成员赋一个值的,下面的City枚举的成员就被指定了一个值,如下所示: //给枚举赋值 enum City: String{ case Beijing = "北京" case ShangHai = "上海" case GuangZhou = "广州" case ShengZhen = "深圳" } 使用枚举变量的rawValue可以获取给每个枚举成员赋的值,代码如下: //定义枚举变量并赋值 var myInCity: City = City.Beijing //获取枚举变量的值 var myInCityString: String = myInCity.rawValue; println(myInCityString) //输出:北京 四、通过枚举成员的值给枚举变量赋值 什么是通过枚举成员的值给枚举变量赋值呢?举个例子,以上面的枚举类型City为例,假如我们只知道一个枚举成员的值是“北京”,而不知道“北京”这个值对应的枚举成员是"Beijing", 在Swift中是可以通过“北京”这个值给枚举变量赋一个枚举成员“Beijing”的。 是不是有点绕啊,来点实例吧,下面就是通过枚举成员的原始值给枚举变量赋值的代码 //通过枚举成员的值,来给枚举成员赋值 var youInCity: City? = City(rawValue: "北京"); 为啥我们的youInCity是可选值类型的呢?原因很简单,我们不确定枚举City中的成员的值是否含有“北京”,枚举变量youInCity的值是不确定的,所以喽是可选类型的,下面我们就取出youInCity的值,首先判断youInCity是否为City.Beijing, 如果是,说明赋值成功就输出值。 //取出youInCity中的值 if youInCity == City.Beijing { var cityName: NSString = youInCity!.rawValue println(cityName) //输出:北京 } 找一个枚举成员的值中不包含的字符串用来给枚举变量赋值,观察一下结果,下面的testCity的值打印出来为nil, 因为枚举中没有一个成员的值为“京”。 //传入一个枚举中没有的值 var testCity: City? = City(rawValue: "京"); // testCity为nil println(testCity) 五、枚举值自增 好东西还是要保留的,在Swift中的枚举值如果是整数的话,第一个赋值后,后边的会自增的。关于枚举值自增就不多说了,直接看实例吧。 //枚举值自增 enum Hour: Int{ case One = 1 case Two case Three case Four case Five case Six case Seven case Eight } var hourTest: Int = Hour.Eight.rawValue println(hourTest) // hourTest = 8 六、枚举关联值 什么是枚举的关联值呢?从字面意思上看是给枚举成员关联一个值,没错,就是在给枚举变量赋值时,给枚举变量关联一个值。在Swift中如何做的呢?就是在声明枚举类型中的元素时使用小括号来制定关联值的类型,然后在给枚举变量赋值的时候关联一个或者多个值即可,直接看实例。 下面的代码是给iOS指定了两个String类型的关联值,在给枚举变量赋值的时候关联两个值。关联这两个值可以在Switch语句中进行使用。 //枚举的关联值 enum mobileLanguage{ case IOS (String, String) case Android (String) } var iPhone: mobileLanguage = mobileLanguage.IOS("Objective-C", "Swift") switch iPhone { case mobileLanguage.IOS(let language1, let language2): println("language1 = \(language1), language2 = \(language2)") case mobileLanguage.Android(let temp): println(temp); default: println("NO") } //输出结果:language1 = Objective-C, language2 = Swift 七、枚举函数 在Swift中的枚举是可以添加函数的,有没有眼前一亮呢。下面的代码段是在上面的关联值代码的基础上添加了一个描述函数,返回的就是当前枚举变量的枚举信息,如下代码段所示: //枚举函数 enum mobileLanguageFun{ case IOS (String, String) case Android (String) //定义枚举函数 var description: String{ switch self { case mobileLanguageFun.IOS(let language1, let language2): return "language1 = \(language1), language2 = \(language2)" case mobileLanguageFun.Android(let temp): return temp default: return ("NO") } } } var myMobile: mobileLanguageFun = mobileLanguageFun.IOS("objc", "swift") println(myMobile.description) //language1 = objc, language2 = swift
之前总结过Objective-C中的字符串《Objective-C精选字符串处理方法》,学习一门新语言怎么能少的了字符串呢。Swift中的String和Objective-C语言中NSString还是区别不小的,Swift中的String又回归了正常状态,使用起来更为方便快捷。本篇博客的主题就是Swift中的字符串类型String,String在Swift中让人省心了不少。今天这篇博客就好好的认识一下Swift中的String。 一、字符串拷贝 在Swift中的字符串拷贝直接可以使用=号来操作,这个等号不是指针之间的赋值这么简单。如果将字符串A的值赋给字符串B,那么A和B的的内存地址是不同的,也就是字符串A和字符串B有各自的内存空间。下面的实例就给我们展示了上面的说法: 1.首先写个输入函数,该函数用于输出字符串的内存地址,代码如下: //打印变量地址的函数 func printVarAddress(tempString: String){ var address = String(format: "%p", tempString) println(address) } 2. 创建一个字符串变量并赋上初始值,然后再定义一个变量,把上一个变量的值通过=号来赋值给这个新的变量,并调用上面的方法来打印这两个变量的内存地址,如下代码所示: var strTemp = "Ludashi" var strTempCopy = strTemp printVarAddress(strTemp) //-- 0x100525d50 printVarAddress(strTempCopy) //-- 0x1005268a0 由上面打印的变量地址可以看出,每个变量都有自己的存储地址,也就是对应着OC中的深拷贝。 二、字符串连接 在Swift中字符串的连接就简单了许多,一个+号搞定,再也不用NSStringFormat了,下面的代码是在Swift中进行字符串连接所使用的方式,和其他编程语言如PHP等是一样一样的。废话少说直接上代码。 //=============字符串连接============== var myFirstName = "Li" var mySecondName = "Zelu" var myName = mySecondName + myFirstName println(myName) // -- ZeluLi 三、字符串遍历 在Swift中的字符串是可以使用for - in 来直接进行遍历的,如下所示: //==============字符串遍历========== var searchString = "Ludashi" for tempChar in searchString { println(tempChar) } 四、字符串的比较 字Swift中的字符串间的比较不是使用isEqualToString方法,直接使用==和!=号就可以搞定,是不是瞬间简单了不少呢。应注意的是,在Swift中的Bool类型的值不再是OC中的Yes Or No了,而是false Or ture了。下面的代码段是对两个字符串通过==和!=来进行比较的。 //字符串比较==与!= var myNameTemp = "lizelu" var myBlogName = "ludashi" var boolOne = myNameTemp == myBlogName // -- false var boolTwo = myNameTemp != myBlogName // -- true println(boolOne) println(boolTwo) 五、Swift中常见字符串函数 1、使用hasPrefix和hasSuffix来判断一个字符串是否为另一个字符串的前缀或者后缀 //判断前缀还是后缀 var isHasPrefixOrSuffix = "我是lizelu" var isPrefix = isHasPrefixOrSuffix.hasPrefix("我") println(isPrefix) // -- ture var isSuffix = isHasPrefixOrSuffix.hasSuffix("zelu") println(isSuffix) // -- ture 2.字符串长度 在OC中获取字符串长度是使用length, 而Swift中则是使用count()全局函数,如下: //字符串长度 var strLenght = count(isHasPrefixOrSuffix) println(strLenght) // -- 8 -- 3.字符串插值 在OC中如果你想往一个字符串中插入一个值的话,那么就得使用字符串的格式化函数了,而在Swift中则使用\()就可以了,具体使用方式如下所示: //字符串插值 var insertToStringValue = 1010101 var strInserReaultValue = "二进制编码\(insertToStringValue)" println(strInserReaultValue) //二进制编码1010101 4.调用NSString方法 在Swift中如果想调用NSString所特有的方法时,该怎么办呢?那么就使用as关键字来转换一下类型吧,就是把String类型通过as操作,转换成NSString类型,然后在调用NSString相应的方法(比如要获取一个字符串指定范围内的字符串时,使用NSString的方法就会更为简单一些)。 //String转成NSSting调用NSSting的方法 var stringToNSString = "SwiftWithMe" var strNs: NSString = "aaa" strNs.length // -- 3 -- //就可以掉哟过NSSting的各工种方法了 var strLength = (stringToNSString as NSString).length // -- 11 -- 今天有关Swift字符串的东西就先到这儿,以后字啊使用Swift做开发实例时,用到Swift字符串时再做补充。
之前更新了一段时间有关Swift语言的博客,连续更新了有6、7篇的样子。期间间更新了一些iOS开发中SQLite、CollectionViewController以及ReactiveCocoa的一些东西。时隔两月,还得继续更新Swift语言的东西不是。在去年翻译《Swift编程入门经典》(Swift1.0版本,基于Xcode6)这本书时,系统的搞了搞Swift语言,接下来的一段时间内打算持续更新一下相关Swift语言的一些东西, 不过现在已经是Swift2.0版本了,区别还是不小的。并且目前在工作中正重构着整个项目的代码,之后根据一些项目实例再更新一些关于代码重构的博客与大家交流一下,然后再整理一些Android开发的一些东西吧,当然是类比着iOS开发了。 废话少说,开始今天博客的主题。有些小伙伴看到今天的博客Title可能会笑到,基本运算符有什么好说的,只要会编程的,都会使用基本运算符。此话不假,但是今天博客的主题不是介绍++i还有i++的区别的。今天博客中介绍那些在Swift中比较独特的基本运算符,这些运算符会让你眼前一亮(有些是在OC语法中渴望使用到的)。不积跬步无以至千里,不积小流无以成江海。虽然需要进阶,但是基础还是蛮重要。今天博客前半部分是需要注意的基础运算符,有基础运算符当然就有高级运算符,接着会介绍一些高级运算符。今天就窥探一下Swift2.0的东西(基于Xcode7.1) 一. 需注意的基础运算符 1. 赋值运算符(=) 在Objective-C,C等一些语言中允许你在表达式中使用=号, 如下所示。 testNumber = 20会返回一个bool类型的值YES。 testNumber = 20在表达式中是永真的。所以下方的代码会打印Log中的内容。 1 NSInteger testNumber = 10; 2 if ((testNumber = 20)) { 3 NSLog(@"testNumber = %ld", testNumber); 4 } 在Swift中是不允许这样做的,从这一点也能看出Swift语言的安全性。如果你在Swift写了上面的代码,就会报出下面的错误。IDE就会提示你,问你是不是应该使用==预算符。 2.类型安全性,不允许隐式类型转换 这一点也是Swift语言的一个优势,在Swift语言中是不允许你使用隐式类型转换的。即便是Double类型和Float类型进行隐式类型转换也是不可以的。而在Objective-C中是可以进行隐式类型转换的。看下方实例: 在Objective-C中你可以这样做, 下方代码是可以编译通过的。两种类型(Float32, Float64)不同的数据进行相加,然后再把结果隐式转换成另一种类型(NSInteger)。 1 Float32 floatNumber1 = 10.0f; 2 Float64 floatNumber2 = 20.0f; 3 NSInteger result = floatNumber1 + floatNumber2; 上面的代码在Swift中如下,IDE会报一个错误,如下所示。错误的大致意思就是你不能把Float32类型的数据与Float64类型的数据进行相加。其本质原因是在Swift语言中是不允许你进行隐式类型 在Swift中对上述代码进行类型显示转换,编译就会通过。在Playground中就会显示相应的结果值。 3.取模运算(%)的特殊性 还是以Objective-C做类比,在Objective-C中取模运算(%)只支持整型,如果在取模运算中你使用了浮点类型,那么就会报出如下错误。大概意思就是取模运算不支持浮点类型,请转换成NSInteger类型。 而Swift中的取模运算就支持浮点类型,上面的语句在Swift中就不会报错,下方是上述事例在Playground中的结果值: 4. nil聚合(合并,连接)运算符(??) 该运算符可谓是Swift中添加的新特性,??运算符在Objective-C中是没有的。但是??不是Swift的原创,在C#中也是有??运算符的,而且用法和Swift中??用法类似。都是用来处理nil值的运算符,通过一个实例来进行介绍,一目了然。 在实例中我们先定义一个可选类型的字符串变量developLanguage,来记录开发语言, 再定义一个选择开发语言的字符串变量selectLanguage。如果developLanguage的值为nil的话,默认选择的语言是“Swift”。 如果developLanguage的值不为nil, 就强制打开可选类型的值,把该值赋值给字符串变量selectLanguage。具体代码如下所示: 接下来就是预算符??出厂的时候了,一个??的功能就是上面代码中if -- else的功能。也就是说上面的if -- else 语句可以使用下方的??运算符来代替。下面要注意一点的是在??运算符中使用可选类型变量时没有使用!强制打开可选类型的值, 因为在??运算符中能确保使用的可选类型变量中有值,如果没有值就不打开使用,所以就可以把!省略掉。 5.比较运算符支持字符串 在Objective-C中你可以使用比较运算符来比较运算符,这样做编译器是不会报错的,但是你不会得到你想要的结果。如果你直接用比较运算符来比较字符串的话实质上是比较的字符串的内存地址,请看下方Objective-C的代码。有下方的输出结果不难看出比较的是字符串的内存地址。 在Swift中你可以使用比较运算符来比较字符串,如下所示: 6.区间运算符 区间运算符可以表示两个值之间的范围。... 是闭区间运算符,比如a...b表示a到b这个区间并且包括a和b的值。 ..<是半开区间, 比如a..<b 表示a到b这个区间的值,不包括b。其用法如下: 下方代码使用的是闭区间运算符1...10, 会循环10次 如果改成半开区间,那么就是循环9次 二. Swift中的高级运算符 1.Swift中的位运算 如果你在大学课程中学过数字电路这门课程的话,想必不会对位运算陌生的。在好多编程语言中也都有位运算。位运算应用得当可以提高算法的效率,在一些高效的算法中有时会用到位运算,再此就不做过多的讨论了。接下来将会搞一搞Swift中的按位与,按位或,按位异或以及按位取反等操作。 (1) 按位与(&) 对二进制中的每一位进行与操作,所以叫按位与。运算规则为1 & 1 = 1, 1 & 0 = 0, 0 & 1 = 0, 0 & 0 = 0。按位与简单的用法就是保留二进制中的指定位数,或者对数值进行清零操作。下方是按位与操作的小实例:0000_1111与1000_1011进行按位与运算,就是保留1000_1011的后四位。如果要对指定的二进制数进行清零的话,只需要把该值和0000_0000进行按位与操作即可。 下方是上述代码中按位与的原理图: (2)按位或(|) 顾名思义,按位或就是对二进制中的每一位进行或操作,所以叫按位或。运算规则为 1 | 1 = 1, 1 | 0 = 1, 0 | 1 = 1, 0 | 0 = 0。按位或常用来把指定位置的数值置为1。下方是实例是要把0000_0011的前四位置为1,后四位不变,所以要与1111_0000进行按位或操作。 按位或操作的原理图如下: (3) 按位异或(^) 异或的运算法则也是比较容易理解的, 简单一句话就是相同为0,不同为1。 1 ^ 1 = 0, 1 ^ 0 = 1, 0 ^ 1 = 1, 0 ^ 0 = 0。有异或的运算规则我们容易得出0 异或任何一个数,还等于这个数的本身。1 异或任何一个数等于这个数取反。下方是一个实例: 上面代码的原理图如下: 异或的用法是比较多的,我们可以使用异或运算在不创建临时变量时来交换两个数的值。具体如下: 我们还可以使用异或运算来判断两个值是否相等,如果两个数异或后的值为0,那么两个数就相等,具体代码如下所示: 1 if swap1 ^ swap2 == 0 { 2 print("swap1 == swap2") 3 } (4) 按位取反(~) 一个数值与1进行异或,都会得到其相反的值,也就是取反。我们还可以通过按位取反运算符来对值进行取反,取反的规则就比较简单了,就是0变成1,1变成0。下方是取反运算的实例,在Playground中可以看出其取反后的值。按位取反的实例如下(下面只讨论的正数的取反,关于负数的取反没有): (5) 按位左移(<<)和按位右移(>>)操作 正数的左右位移用0来填补空位,而负数左移用0来填补,右移用符号位来填补。实例如下: 2.溢出运算符 在Swift语言中,如果值溢出,是会报错的。这也能反映出Swift安全性,如果你想在值溢出时对有效位进行截断的话,那么你就可以使用溢出运算符。 值上溢出运算符(&+), 关于值上溢运算符,就不说多少废话了,直接上实例。在Playground中取出UInt8类型的上限,然后对其加1,让其溢出。如果你直接使用+号的话,会给出一个错误。使用&+就不一样了,效果如下。值的下溢运算符(&-, &*)的用法和&+类似,在此就不做赘述了。(&/与&%)在Xcode7中未编译通过,提示找不到此标示符。 3. 运算符重载 在Swift2.0中运算符重载是比较容易实现的,就是把函数名换成你要重载的运算符即可。下方就通过一个小实例来看一下Swift中的运算符重载。在Swift中是+号运算符是不支持元组直接相加的,如果你直接对元组进行加法操作,会报下面的错误。 (1)对中缀运算符重载,如果对+运算符进行重载,那么+运算符将会支持元组相加, 具体代码和运行结果如下所示,+运算符原来的功能还是不变的。 (2)对前缀运算符进行重载,就以-运算符为例。对前缀运算符重载在func前面要加上prefix修饰符。如果要对后缀运算符进行重载的话,要使用postfix进行修饰,下方是对-进行前缀运算符重载。具体代码如下: //前缀运算符重载 struct Point { var x = 0.0, y = 0.0 } prefix func - (point: Point) -> Point { return Point(x: -point.x, y: -point.y) } let positive = Point(x: 3.0, y: 4.0) let negative = -positive 结果输出如下: (3) 自定义运算符:在Swift中支持定义属于你自己的运算符,在定义运算符时,先使用operator 声明一下所指定的标示符,并且指定一下是前缀还是后缀等,具体的就看下面的代码即可: //自定义运算符 //1、先声明自定义的运算符 prefix operator +++ {} //2.进行实现 prefix func +++ (point:Point) -> Point{ return Point(x:point.x + 1, y:point.y + 1); } let aaa = Point(x: 1.0, y:2.0); let add = +++aaa; print(add) // Point(x: 2.0, y: 3.0)
今天的博客算是比较基础的,还是那句话,基础这东西在什么时候都是最重要的。说到函数,只要是写过程序就肯定知道函数是怎么回事,今天就来讨论一下Swift中的函数的特性以及Swift中的闭包。今天的一些小实例中回类比一下Objective-C中的函数的写法等等。Swift中的函数还是有许多好用的特性的,比如输入参数,使用元组返回多个值, 定义形参名,设定默认参数以及可变参数等等一些好用的特性。而在Swift中的闭包就是Objective-C中的Block, 除了语法不通外,两者的用法是一样的。废话少说,开始今天的主题,先搞一搞Swift中的函数,然后在搞一搞Swift中的闭包。 一.Swift中的函数 1. 函数的定义与使用 在介绍Swift中的函数之前,我想用Objective-C中的一个简单的加法函数来作为引子,然后类比着实现一下Swift中相同功能的函数。关于函数定义就比较简单了,就是一些语法的东西,下面的代码片段是Objc中求两个整数之和的函数,并返回两个数的和。 - (NSInteger)sumNumber1:(NSInteger) number1 number2:(NSInteger) number2 { return number1 + number2; } 函数的功能比较简单了,就是把两个整数传进来,然后返回两个整数的和。接下来将用Swift语言实现,也好通过这个实例来熟悉一下Swift语言中定义函数的语法。下方是Swift语言中求两个整数之和的函数。语法比较简单了,在Swift中定义函数,我们会使用到关键字func来声明函数。1 //函数定义 2 func sum (number1:Int, number2:Int) -> Int{ 3 return number1 + number2; 4 } 用文字来描述Swift定义基本函数的语法就是: func 函数名 (形参列表) -> 返回值类型 { 函数体},这样你就可以定义一个函数了。当然,函数定义时还有好多其他的用法,下面会详细介绍。上面函数的调用方法如下:1 let sumTwoNubmer = sum(2, number2: 3); 2. 函数中的形参列表 关于函数中的形参列表还是有必要提上一嘴的,因为形参列表作为函数数据源之一,所以把参数列表好好的搞一搞还是很有必要的。参数列表也有很多好用的使用方式,接下来详细的介绍一下函数的形参列表。 (1) 默认的形参是常量(let) 在函数的形参列表中,默认的形参是常量。也就是相当于用let关键字对形参进行修饰了。我们可以做个试验,把上面加法函数做一个修改,在加法函数中对number1进行加1操作,你会得到一个错误,这个错误的大体意思就是“number1是不可被修改的,因为它是let类型的常量”。并且编译器还给人出了Fix-it(修复)的方案,就是在number1前面使用var关键字进行修饰,使其成为变量,这样才可以修改其值。 上面说这么多,一句话:形参默认是常量,如果你想让其是变量,那么你可以使用var关键字进行修饰,这样被关键字var修饰的变量在函数中就可以被修改。下方就是报的这个错误,和编译器提供的解决方案。(在Objc中默认可以在函数中改变形参的值) (2)给形参命名 为了代码的可读性和可维护性,我们在定义函数时,需要为每个参数名一个名字,这样调用者见名知意,很容易就知道这个参数代表什么意思了。接下来还是在上述加法函数中进行修改,为每个参数名一个名字,并看一下调用方式。修改上面的函数,给第一个形参命名成numberOne, 第二个形参为numberTwo, 下方是修改后的函数。 紧接着sum()函数的调用方式也会有所改变,在调用函数时编译器会给出参数的名称,这样调用者一目了然。 //函数定义 func sum (numberOne number1:Int, numberTwo number2:Int) -> Int{ return number1 + number2; } let sumTwoNubmer = sum(numberOne: 10, numberTwo: 20); 调用上述函数时,下方是编译器给出的提示,一目了然呢。 关于Swift中参数名的内容,要说明的是在Swift1.0的时候,你可以在参数前面添加上#号,然后参数名就与变量(或者常量)的名字相同,而Swift2.0后这个东西去掉了,因为默认就相当于Swift1.0中添加#号。 (3) 函数的传参与传引用 先暂且这么说着,在C语言的函数中可以给函数传入参数,或者传入实参的内存地址就是所谓的传引用。如果传入的是引用的话,在函数中对值进行修改的话,那么出了函数,这个被修改的值是可以被保留的。在Swift中也是可以的,不过你需要使用inout关键字修饰形参,并且在使用该函数时,用&来修饰。这一点和C语言中类似,&就是取地址符。下方是inout使用的一个小实例。 1 func incrementStepTwo (inout myNumber:Int) { 2 myNumber += 2 3 } 4 var myTestNumber = 6 5 incrementStepTow(&myTestNumber) //myTestNumber = 8 myTestNumber变量经过incrementStepTwo()函数后,其值就会增加2。当然前提是myTestNumber是变量,如果myTestNumber是常量的话,那么对不起,调用该函数就会报错,下面是把var改成let后IDE给的错误提示。错误原因很显然是你动了一个不该动的值,也就是常量不可再次被修改的。 (4) 不定参数函数 不定参数函数也就是形参的个数是不定的,但是形参的类型必须是相同的。不定形参在使用时怎么取呢?不定个数的形参实际上是一个数组,我们可以通过for循环的形式来遍历出每个形参的值,然后使用就可以了。下方incrementMultableAdd()函数的形参的个数是不定的,其功能是求多个整数的和。在函数中我们只需遍历每个参数,然后把每个参数进行相加,最后返回所求的和即可。函数比较简单,再此就不在啰嗦了。 (5) 默认形参值 在Swift语言中是支持给形参赋初始值的,这一点在其他一些编程语言中也是支持的。但是Objective-C这么看似古老的语言中就不支持给形参指定初始值,在Swift这门现代编程语言中是支持这一特性的。默认参数要从参数列表后开始为参数指定默认值,不然就会报错。下方就是为函数的形参指定默认参数的示例。一个表白的方法sayLove(), 形参youName默认是“山伯”, 形参loverName默认是“英台”。 紧接着是sayLove函数的三种不同的调用方式,在调用函数时你可以不传参数,可以传一个参数,当然传两个也是没问题的。 因为函数的每个参数都是有名字的,在含有默认参数的函数调用时,可以给任意一个参数进行传值,其他参数取默认值,这也是Swift的一大特色之一,具体请看如下简单的代码示例: 3.函数类型 每个函数都有自己的所属类型,函数类型说白了就是如果两个函数参数列表相同以及返回值类型相同,那么这两个函数就有着相同的函数类型。在Swift中可以定义一个变量或者常量来存储一个函数的类型。接下来将用过一个实例还介绍一下函数类型是个什么东西。 (1) 首先创建两个函数类型相同的函数,一个函数返回两个整数的差值,另一个函数返回两个整数的乘积。当然这两个函数比较简单,直接上代码: //现定义两个函数类型相同的函数 func diff (number1:Int, number2:Int) -> Int { return number1 - number2; } func mul (number1:Int, number2:Int) -> Int { return number1 * number2; } (2) 函数定义好后,接着要定义个一个枚举来枚举每种函数的类型,下面定义这个枚举在选择函数时会用到,枚举定义如下:1 //定义两种计算的枚举类型 2 enum CountType:Int { 3 case DiffCount = 0 4 case MulCount 5 } (3) 接下来就是把(1)和(2)中定义的东西通过一个函数来组合起来。说白了,就是定义个函数来通过枚举值返回这个枚举值所对应的函数类型。有时候说多了容易犯迷糊,就直接上代码得了。下方函数的功能就是根据传进来的枚举值来返回相应的函数类型。//选择类型的函数,并返回相应的函数类型 func choiseCountType(countType:CountType) -> ((Int, Int) -> Int) { //函数类型变量 var myFuncType:(Int, Int) -> Int switch countType { case .DiffCount: myFuncType = diff case .MulCount: myFuncType = mul } return myFuncType; } (4) 接下来就是使用(3)中定义的函数了,首先我们需要定义一个相应函数类型((Int, Int) -> Int)的变量来接收choiseCountType()函数中返回的函数类型,然后调用该函数类型变量,在Playground中执行的结果如下: 4.函数嵌套 我们可以把 3 中的代码使用函数嵌套进行重写,在Swift中是支持函数嵌套的。 所以可以吧3.1和3.2中的函数放到3.3函数中的,所以我们可以对上述代码使用函数嵌套进行重写。使用函数嵌套重写后的代码如下所示,当然,choiseCountType()函数的调用方式没用发生改变,重写后的调用方式和3.4中的调用方式是一样一样的。 //选择类型的函数,并返回相应的函数类型 func choiseCountType(countType:CountType) -> ((Int, Int) -> Int) { //现定义两个函数类型相同的函数 func diff (number1:Int, number2:Int) -> Int { return number1 - number2; } func mul (number1:Int, number2:Int) -> Int { return number1 * number2; } //函数类型变量 var myFuncType:(Int, Int) -> Int switch countType { case .DiffCount: myFuncType = diff case .MulCount: myFuncType = mul } return myFuncType; } 二. 闭包 说道Swift中的闭包呢,不得不提的就是Objective-C中的Block, 其实两者是一个东西,使用方式以及使用场景都是相同的。我们完全可以类比着Objective-C中的Block来介绍一下Swift中的Closure(闭包)。其实就是匿名函数。接下来的这段内容,先介绍一下Swift中Closure的基本语法,然后在类比着ObjC中的Block窥探一下Closure的使用场景。 1.Closure变量的声明 Closure就是匿名函数,我们可以定义一个闭包变量,而这个闭包变量的类型就是我们上面介绍的“函数类型”。定义一个闭包变量其实就是定义一个特定函数类型的变量,方式如下。因为Closure变量没有赋初始值,所以我们把其声明为可选类型的变量。在使用时,用!强制打开即可。 1 var myCloure0:((Int, Int) -> Int)? 除了上面的方式外,我们还用另一种常用的声明闭包变量的方式。那就是使用关键字typealias定义一个特定函数类型,我们就可以拿着这个类型去声明一个Closure变量了,如下所示1 //定义闭包类型 (就是一个函数类型) 2 typealias MyClosureType = (Int, Int) -> Int 3 var myCloure:MyClosureType? 2. 给Closure变量赋值 给Closure变量赋值,其实就是把一个函数体赋值给一个函数类型的变量,和函数的定义区别不大。但是给闭包变量赋值的函数体中含有参数列表,并且参数列表和真正的函数体之间使用关键字in来分割。 闭包可选变量的调用方式与普通函数没什么两样,唯一不同的是这个函数需要用!来强制打开才可以使用。赋值和调用方式如下。 3. 闭包回调的应用实例 暂且先称作闭包回调吧,其实就是Objc中的Block回调。在Swift中的闭包回调和Objc中的Block回调用法一致,下方将会通过一个实例来介绍一下闭包的应用之一。下方会创建两个视图控制器,我们暂且称为FirstViewController和SecondViewController。在FirstViewController上有一个Label和一个Button, 这个Button用来跳转到SecondViewController, 而这个Label用来显示从SecondViewController中回调过来的值。 而SecondViewController也有一个TextField和一个Button, 点击Button就会把输入框中的值通过闭包回调回传到FirstViewController然后在FirstViewController上的Label显示。 (1) 构建这个实例的第一步要做的就是使用Stroyboard把我们所需的控件布局好,并且管理相应的类。当然我们这个Demo的重点不在于如何去布局控件,如何去关联控件,以及如何去使用控件,所以上述的这些就不做赘述了。这个实例的重点在于如何使用Closure实现值的回调。下方是我们的控件布局和目录结构的截图,从Storyboard上的控件来看,功能也就一目了然了。点击“FirstViewController” 上的“Go SecondViewController”按钮,就会跳转到 “SecondViewController” 。 在SecondViewController视图上的输入框输入数值,点击Back按钮返回到FirstViewController, 同时把输入框中的文本通过闭包回调的形式回传过来在FristViewController的label上显示。大致就这个简单的功能。 (2)FirstViewController.swift中的内容 FirstViewController.swift中的内容比较简单,就关联一个Label控件和一个按钮点击的事件,点击按钮就会跳转到SecondViewController,具体代码如下,在此就不啰嗦了,请看代码中的注释。下方代码重要的一点是在跳转到SecondViewController时要实现其提供的闭包回调,以便接受回传过来的值。 // // FirstViewController.swift // SwiftDemo // // Created by Mr.LuDashi on 15/11/18. // Copyright © 2015年 ZeluLi. All rights reserved. // import UIKit class FirstViewController: UIViewController { @IBOutlet var showTextLabel: UILabel! //展示回调过来的文字信息 override func viewDidLoad() { super.viewDidLoad() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } //点击按钮跳转到SecondViewController @IBAction func tapGoSecondViewControllerButton(sender: UIButton) { //从Storyboard上加载SecondViewController let secondVC = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("SecondViewController")as! SecondViewController //实现回调,接收回调过来的值 secondVC.setBackMyClosure { (inputText:String) -> Void in self.showTextLabel.text = inputText } //push到SecondViewController self.navigationController?.pushViewController(secondVC, animated: true) } } (3) SecondViewController.swift中的内容 SecondViewController.swift中的内容也不麻烦,就是除了关联控件和事件外,还定义了一个闭包类型(函数类型),然后使用这个特定的函数类型声明了一个此函数类型对应的变量。我们可以通过这个变量来接受上个页面传过来的闭包体,从而把用户输入的值,通过这个闭包体回传到上个页面。具体代码实现如下: // // SecondViewController.swift // SwiftDemo // // Created by Mr.LuDashi on 15/11/18. // Copyright © 2015年 ZeluLi. All rights reserved. // import UIKit typealias InputClosureType = (String) -> Void //定义闭包类型(特定的函数类型函数类型) class SecondViewController: UIViewController { @IBOutlet var inputTextField: UITextField! //输入框,让用户输入值,然后通过闭包回调到上一个页面 var backClosure:InputClosureType? //接收上个页面穿过来的闭包块 override func viewDidLoad() { super.viewDidLoad() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } //闭包变量的Seter方法 func setBackMyClosure(tempClosure:InputClosureType) { self.backClosure = tempClosure } @IBAction func tapBackButton(sender: UIButton) { if self.backClosure != nil { let tempString:String? = self.inputTextField.text if tempString != nil { self.backClosure!(tempString!) } } self.navigationController!.popViewControllerAnimated(true) } } (4) 经过上面的步骤这个实例已经完成,接下来就是看一下运行效果的时间了。本来想做成Git动态图的,感觉实例功能简单,而且UI上也比较简单,就没做,还是看截图吧。运行效果的截图如下: 4.数组中常用的闭包函数 在Swift的数组中自带了一些比较好用的闭包函数,例如Map, Filter, Reduce。接下来就好好的看一下这些闭包,用起来还是比较爽的。 (1) Map(映射) 说到Map的用法和功能,不能不说的是如果你使用过ReactiveCocoa框架,那么对里边的Sequence中的Map的使用方式并不陌生。其实两者的使用方法和功能是极为相似的。如果你没使用过RAC中的Map,那也无关紧要,接下来我们先上段代码开看一下数组中的Map闭包函数。 通过上面的代码段以及运行结果,我们不难看出,map闭包函数的功能就是对数组中的每一项进行遍历,然后通过映射规则对数组中的每一项进行处理,最终的返回结果是处理后的数组(以一个新的数组形式出现)。当然,原来数组中的元素值是保持不变的,这就是map闭包函数的用法与功能。 (2) Filter (过滤器) Filter的用法还是比较好理解的,Filter就是一个漏勺,就是用来过滤符合条件的数据的。在ReactiveCocoa中的Sequence也是有Filter的,用法还是来过滤Sequence中的数据的。而在数组中的Filter用来过滤数组中的数据,并且返回新的数组,新的数组中存放的就是符合条件的数据。Filter的用法如下实例,下方的实例就是一个身高的过滤,过滤掉身高小于173的人,返回大于等于173的身高数据。 (3)Reduce 在ReactiveCocoa中也是有Reduce这个概念的,ReactiveCocoa中使用Reduce来合并消减信号量。在swift的数组中使用Reduce闭包函数来合并items, 并且合并后的Value。下方的实例是一个Salary的数组,其中存放的是每个月的薪水。我们要使用Reduce闭包函数来计算总的薪水。下方是DEMO的截图:
说到数组和字典,只要是编过程的小伙伴并不陌生。在Swift中的数组与字典也有着一些让人眼前一亮的特性,今天的博客就来窥探一下Swift中的Array和Dictionary。还是沿袭之前的风格,在介绍Swift中的数组时,我们会对比一下ObjC中的数组和字典,因为ObjC也是iOS开发的主要语言不是。无论是简单还是复杂的程序,数组和字典的用处还是比较多的,这两者虽然是Swift的基础内容,但是也不失其重要性。关于Objc的集合类请参考之前的博客《Objective-C中的集合类》。 一、Swift中的数组(Array) 数组在其他编程语言中都是存在的,数组就是一组数的集合。虽然其他编程语言中都有数组,但是不同语言中的数组都有着一些属于自己的特性。Swift也不例外,Swift中的数组使用起来还是比较人性化的. 1.数组的声明与创建 在聊Swift中的Array声明创建之前,我想聊一下ObjC中数组的声明与创建,下方是ObjC中不可变数组和可变数组的创建方式。在ObjC中可以使用NSArray来声明不可变数组,使用NSMutableArray来声明可变数组。 1 NSArray *objcArray01 = [NSArray array]; 2 NSArray *objcArray02 = @[@(1), @(2)]; 3 4 NSMutableArray *mutableArray = [NSMutableArray array]; 而在Swift语言中,你可以使用关键字let来声明不可变数组,使用var来声明可变数组,下方代码声明的是Swift中的可变数组的几种方式。有一点需要注意的是ObjC中的数组只允许往里面存储对象,而不允许往里存储基本数据类型(Int, Float等)。而在Swift中是允许把基本数据类型存入数组中的,如下方代码段所示。下方给出了数组的三种定义的方式。后两种给数组元素制定了数据类型,表示该数组中只能够存储Int类型的值,如果你往里存入了其他的值,对不起,编译器会报错的。 在Swift的数组中,如果在声明数组时没有指定数组中元素的数据类型,那么数组中是可以存放不同数据类型的数据的。如果你指定了数组中只允许存储一种数据类型,如果你再往里存储其他数据类型的话,那就是你的不对了,编译器回给你指出你的错误。具体如下所示: 2.数组的操作 (1)可变与不可变数组 如果你想对数组中的元素进行增加、修改或者删除,那么你需要把Array定义成可变数组。如果你把数组定义成了不可变数组,但是你又对他进行了操作,那就是你的不对了。无论是ObjC还是Swift都不允许对不可变数组这位高冷的“御姐”动手动脚,不然会抛出一个错误的。但是可变数组就不一样了,你可以对可变数组进行增删改查操作。 下图是在ObjC中对NSArray操作后的结果,你不能对NSArray中的元素进行修改,也就是说你对NSArray的操作权限只有读的权限,没有写的权限。如果你想对数组进行读写的权限的话,那你就有必要使用可变数组NSMutableArray了。使用NSMutableArray就不会报错,因为你对它有读写的权限。 在Swift中的可变数组和不可变数组归根结底还是变量和常量也就是var和let关键字的使用了。你对变量持有读写的权限,对常量持有读的权限。下方的小实例,本质还是对let和var的讨论,下方是Swift实例: (2) 插入元素 上面Swift实例中已经展示了如何往数组中插入元素。Swift中往数组中插入元素和OC中使用的方法是一样的,只是调用方法的方式不同。下方代码就是往可变数组arrayTest中的索引1的位置插入一个值“Objc”。语法比较简单就不做过多的赘述了。 1 arrayTest.insert("Objc", atIndex: 1); (3) 元素以及数组的追加 你可以使用append函数来往数组的尾部追加值。如果你想把另一个数组追加到一个数组的后方,你可以使用+=运算符来操作。使用+=可以连接数组,这也是Swift中令人兴奋的一个特点。具体操作请看下方的实例: (4)移除元素 上面是添加,接下来要搞一下移除元素。其实Swift中数组移除的方法名和用法和Objective-C中移除可变数组中的元素的方式极为相似。下方截图中是经常使用的移除方法,用法都没有什么难理解的地方,在此就简单的聊上一嘴。 Void removeAll(): 移除数组中全部元素,有一个可选参数,keepCapacity。如果keepCapacity = YES的话,那么数组移除元素后,其存储空间还是存在的,在此往里存储值,不需再给他分配存储空间了。如果keepCapacity=NO的话,那么数组的存储空间就会被回收掉。 String removeAtIndex(): 移除指定索引的元素,并且返回被移除的元素。 String removeFrist(): 移除第一个元素,并返回被移除的元素。 Void removeFirst(n: Int): 这个函数的意思是移除数组前方的几个元素,如果n = 1, 的话就移除前面一个元素这时与String removeFirst()函数功能相同。如果n = 3, 就移除前3个元素。 String removeLast(): 移除数组中最后一个元素。 Void removeRange(subRange: Range<Int>): 这个函数就比较全能呢,它可以移除数组中有效范围内的连续元素。它需要一个Range参数,下方是这个函数的使用方法,Range的起始位置是1,结束位置是2,就是移除索引1到2之间的元素。可以看Playground中的提示。 Range(start:1, end:2) 代表着半开区间1..<2。 3. 使用Array构造函数给数组赋初始值 在一些应用场景中我们需要为数组中的每一项进行初始化,也就是给数组的每一项赋上初始值。举个栗子~,比如我们要使用一个数组来记录公司每个季度的销售额,在数组初始化时,我们给给数组初始化4个初始值为零的元素。接下来就使用Array构造函数来做一些事情。下方就是在数组创建时给数组赋初始值。 二、Swift中的字典(Dictionary) Swift中字典和Objective-C中的字典除了语法不一样外,用法也是大同小异的。字典中存放的就是Key和Value也就是键值对。可以通过Key来取出Value的值,在PHP这门强大的语言中,Array和Dictionary就是一个东西。其实数组就是一种特殊的字典,数组的key就是其下标,只不过这个下标不需要你去指定,由系统分配好,并且是元素在数组中是按下标从小到大排列的。在字典中的键值对是没有固定顺序的。 1. 字典的创建 在Swift中字典的创建和数组的创建也是类似的,就是比数组的item多了一个key。下方创建了两个字典,第一个字典是指定了key与value的数据类型,第二个字典没有为键值指定固定的数据类型。从Playground中我们容易的看出字典中的数据是没有固定数据的,因为读取字典中的值是通过key-value的形式而不是通过下标的方式。下方创建的都是可变字典,因为我们使用的是var关键字进行修饰的,如果你要创建不可变数组,我们就可以使用let关键字进行创建。关于let创建字典的内容,在此就不做过多的赘述了,因为只需要把下方的var换成let即可。 2. 字典item的增删改查 (1) 查询字典的值(读取) 我们可以通过字典键值对的key来取出key对应的value。 下方的代码是分别取出“大毛”和“小黄”的值,如下所示: (2)字典的遍历 只取一个值那多不爽呢,下面介绍一下字典的遍历方法。下方是遍历输出字典myDog中所有的key,字典的keys属性是获取字典中所有的key值。 上述代码段输出结果如下,由输出结果可知,每个Value是可选类型的数据: key: Optional("黄2") key: Optional("小黄") key: Optional("大黄") =============================================================================== 上面是获取字典中的所有keys, 那么下面是获取字典中的所有values, 具体代码如下所示: 上述代码段输出的结果值如下: 1 value:黄2 2 value:小黄 3 value:大黄 =============================================================================== 接下来就是对字典的整个item进行遍历,每个item遍历出来是一个元组。元组的内容是(key, value)。遍历出来的元组我们就可以通过使用元组的形式来获取字典的key和value。具体代码如下所示: 输出结果如下: 元组:(2, "黄2") key: 2 value: 黄2 元组:(3, "小黄") key: 3 value: 小黄 元组:(1, "大黄") key: 1 value: 大黄 (3) 字典的修改 字典元素的修改是比较简单的,下方通过两种。第一种是通过key的形式直接给key对应的value赋值,不过这种修改方式是不会返回被修改的值的,具体代码如下: 如果你想在修改值的时候返回被修改的原始值的话,你需要使用updateValue(forKey:)方法进行元素的修改。该函数是可以返回被修改的原始值的,具体实例如下所示: (4) 增加元素 往已有的可变字典中增加元素就简单多了,直接通过key就可以给value赋值。在可变字典中,如果key已经存在,那么就变成上面的修改字典的值了。可以这么理解,通过key给value赋值时,如果key存在,就覆盖原有的键值对,如果不存在就添加。下方就是往字典中进行添加元素的语句: (5)移除元素 下方截图中是所有字典移除元素的方法, removeAll():移除所有元素。 removeAll(keepCapacity: Bool):移除所有元素, 如果keepCapacity为True,则保留字典原有的存储空间。 removeValueForkey(key: Hashable): 这个方法是通过key来移除元素的。 removeAtIndex(index: DictionaryIndex<Hashable, Value>):这个是通过Dictionary的索引来删除元素的。但是字典的item是没有顺序的,那么我们怎么获取这个DictionaryIndex呢? 那么我们就会使用到另一个函数,就是indexForKey。具体用法如下,移除元素成功后会以元组的形式返回被删除的值。
之前初识Swift中的Switch语句时,真的是让人眼前一亮,Swift中Switch语句有好多特有而且特好用的功能。说到Switch, 只要是写过程序的小伙伴对Switch并不陌生。其在程序中的出镜率还是比较高档。Switch属于程序的分支语句,Switch的功能便于处理多个分支的较为复杂点的逻辑分支。能用Switch实现的代码都可以使用多个if-else分支语句进行替换。 今天这篇博客就是要看一下Swift中的Switch的不同之处,来总结一下Switch不同的特性。在Swift语言中的Switch中,你可以匹配数字,字符串,元组,字符区间等。并且还可以进行数值绑定,以及在case中使用where子句(SQL中经常使用Where子句)。默认的Switch语句中是不需要添加break语句的,因为其默认就是只执行一个case语句就结束,除非你添加上fallthrough。具体的内容还是看下方的小实例吧。 一、Switch语句的匹配类型 Switch语句不仅像其他语言那样支持匹配数字,在Swift语言中的Switch还支持其他现代编程语言所不支持的数据类型,比如字符串,元组,字符区间等。下方会通过一系列的实例来介绍一下Switch语句的用法。在Switch语句中,默认是执行一条符合的case就结束整个Switch语句,如果你想移除执行多个case子句,后面的部分会介绍到。Switch和枚举一般是形影不离的,鉴于本篇博客是介绍Switch语句的,所以我们就不使用枚举定义匹配项了,不过在正式开放中,最好还是把匹配项定义成枚举的形式,关于Swift中的枚举的内容请参考前面发布的一片博客《窥探Swift之别样的枚举类型》 1.使用Switch匹配数字 使用Switch匹配数字这部分和其他语言一样,不过Swift中的Switch是不需要使用break语句的,因为其默认执行完匹配项就跳出Switch语句了。下方是把数字1,2,3转换成“老大”,“老二”,“超生了”的字符串。由实例可知,匹配完 2 后,即使没加break语句,也不会继续执行后边的case语句了,这一点还是比较人性化的。实例比较简单,就不做过多赘述了。 2.使用Switch匹配字符串 这是一个非常值得兴奋的一个特性,在其他一些编程语言中(比如OC)是不支持匹配字符的。在Swift中的Switch语句就支持匹配字符串。下方是一个匹配美女的Switch-case的一个实例,如果你碰到“凤姐”这样的大“美女”,那么你就只能呵呵啦~。请看下方实例: 3.使用Switch语句匹配数字区间 这个特性也是灰常不错的,在Swift中有区间运算符(1...5 表示1到5这个区间中的所有整数)。如果匹配的值是整数,那么我们可以在Case匹配条件中使用区间。下方就是一个匹配数字区间的一个实例,salary是月薪,我们将通过这个月薪分一下等级,这时我们就会用到区间了。具体如下: 4.匹配元组 元组可以可以作为case的条件的,元组在case中的用法还是比较灵活的。在case条件中,我们可以使用下划线来忽略元组中的某一项。并且我们还可以在元组中的一些项中使用区间运算符。在元组匹配中如果你想忽略掉元组中的一项,可以使用下划线进行忽略即可。下方就是一个元组匹配的实例,如下所示。 上面的例子是在匹配元组时忽略不需要匹配的元组匹配项,接下来我们还可以把不匹配的值通过参数的形式传到case后方的子语句块中。这也就是Switch中的数值绑定,具体实例如下所示: 二、Where子句 在SQL中Where子句的出镜率是比较高的,但是出现Switch-Case分支语句中实不多见。Where子句在SQL中后面跟的是条件,在Switch-Case中的Where子句也是如此,后方跟的也是条件。Where子句要结合着上方的介绍的数值绑定来使用,加上Where子句,Swith-Case分支语句的功能就更为强大,可谓是提升了一个能量级。下方是在元组匹配中结合数值绑定使用Where子句,Case和Where后的条件都满足时才会执行该Case后方的语句块。具体实例如下: 上面的实例是在元组匹配中结合着数值绑定来使用的Where子句,下方我们是在字符串匹配中使用Where子句。道理是一样的,就不做过多赘述了,直接代码走起: Switch-Case语句中引入Where子句可谓是完全可以替代if - else 语句的分支。Where子句使得Switch-Case语句中的条件更为灵活多变,使其更为强大。这一点也是Swift中Switch的强大之处。 今天的博客就先到这儿,关于if-esle分支语句,for, while等循环语句就不做过多赘述了。因为其比较较简单,没有太多让人眼前一亮的东西。
说到结构体和类,还是那句话,只要是接触过编程的小伙伴们对这两者并不陌生。但在Swift中的Struct和Class也有着令人眼前一亮的特性。Struct的功能变得更为强大,Class变的更为灵活。Struct中不仅可以定义属性,并且还可以在其中定义函数,这一点比较像Class的特性了。不过Struct毕竟是结构体,它还是不支持继承等类特有的属性的。今天这篇博客就正儿八经的来搞一搞Swift中的Struct和Class。 当然,这篇博客是比较基础的,但是基础的东西才是重要的东西呢,废话不多说了,走起。 一. Struct (结构体) 结构体,说白了就是一组变量,这些变量有统一的命名。在Swift中,我们不仅可以在Struct中声明变量并为变量设置默认值,而且可以在Struct中添加相应的函数。接下来我们就要创建一个Point结构体,里边有两个属性x坐标和y坐标,并且x坐标和y坐标的初始值为0。其中还有两个方法,一个是display方法,负责输出点的坐标,并且还有一个setPoint方法,这个方法负责设置坐标点。然后我们就要去这个结构体去声明变量,并且调用其中的方法。 1.结构体类型的定义 使用struct关键字来声明我们的结构体类型,结构体类型的名称为MyPoint, 其中坐标x, y为变量,其初始值为0。并且为我们的结构体添加了一个setMyPoint()方法和display()方法。 由上面的代码片段你也许会注意到在setMyPoint()方法关键字func前边多了一个mutating关键字。在Struct中的函数,默认(不添加mutating关键字)的函数对Struct中属性只有只读权限。如果你添加上mutating, 那么该函数就对属性持有读写的权限。 2.结构体类型变量的声明以及结构体函数的使用 接下来我们就利用上述“MyPoint”结构体类型来创建一个结构体类型变量。因为(x, y)值是有初始值的,所以在初始化结构体变量时不需要为其指定初始值。Struct类型的使用和Class使用是大同小异的。MyPoint()就类似于类的构造函数。我们声明完变量,并分配内存空间后,我们对x, y的值进行打印可以看到x, y的初始值为0。具体如下所示: 我们可以调用访问权限为读写的setMyPoint()方法来改变结构体变量中属性的值,下方就是把坐标(10.0,20.0)赋值给myPoint变量,具体如下所示。 除了上述方法给结构体变量中的属性赋值外,我们还可以通过构造函数给其属性赋值。也就是在给变量分配内存空间时为其指定初始值,这一点就和类的构造函数即为相似了。具体方式如下图片中的代码片段所示: 结构体就先聊到这儿,下面开始比较重要的部分:类(Class) 二. 类(Class) Swift作为一门现代面向对象编程语言,怎么能没有类呢。关于Objc中的Class, 请参考我之前发表的一篇博客《在Objective-C中浅谈面向对象》, 其中浅谈了Objective-C中面向对象的东西。今天就聊聊Swift中的类,虽然语言不通,但是Class还是大同小异的。本篇博客的此部分注重Swift类中的语法已经使用方式,对面向对象的思想没有做过多的陈述,因为我们的重点是在Swift编程,而不是面向对象编程。好~进入这一部分的主题。 1. 类的创建与构造器 为了简单也是秉着由浅入深的原则,接下来将把上面MyPoint结构体类型使用类的形式来实现一下。简单从语法上看两者是大同小异的。下方截图中的代码段是在上述MyPoint结构体修改而来的。改成下方MyPoint类做了两点修改,第一点就是把struct关键字改成class关键字, 下方是MyPoint类的使用方法,虽然在上述类中没有构造函数,会自动生成一个默认的无参构造函数。如下所示,调用的就是默认的无参构造函数进行的类的实例化。因为在类定义时我们为类中的属性(即类的特征)指定了初始值,所以将值进行打印就会显示初始值了。 你在类定义时,没有为其定义其他的构造函数,如果你调用了该未定义的构造函数,那么就是你的不对了,编译器就会报错了,如下所示: 接下来我们就要为我们的MyPoint()类创建构造函数了。与其他现代编程语言(如C++, C#,Java等)不同,Swift的构造函数不是与类名同名的函数,而是使用特定的函数名init()来创建其构造函数。下方就是我们MyPoint类的构造函数,函数名当然是init了。在构造函数的形参列表中,我们可以为形参指定默认值,虽然下方只是一个构造函数,但是该构造函数与他的形参列表中的默认值一组合起来,可谓是打了一个漂亮的组合拳,使用起来也是灰常顺手的。 给构造函数的形参列表指定默认值就省去了重载构造函数的麻烦。上面添加了一个构造函数,并为各个形参指定默认值,下方是其不同的调用方式,这在C++中应该重载4个构造函数才能实现的效果。Swift语言由此可见一斑呢~为之又眼前一亮,心中为之一振呢。具体调用方式如下: 2.对象的赋值与比较 在Swift中也是允许把一个类的变量的值通过赋值运算符(=)来赋值给另一个变量的。不过有一点要搞明白,如果类变量a的值赋值被类变量b,那么变量a和b就指向同一块内存区域。如果a中的实例变量中的值进行了修改,那么实例b中的值也会进行修改。为了更好的表达这个思想,我们还是来张原理图来介绍一下对象的赋值吧。具体的原理图如下所示: 上面是原理,下方就是验证。我们就声明两个变量a, b。 给a分配一个实例的空间,然后把a赋值给b。再接着就是改变a的值,观察b中的属性变化。具体如下所示: 如果要判断两个变量是否指向同一个实例,那么我们就需要使用恒等运算符(===)了。下方就是判断a是否和b指向同一个内存空间,具体代码如下所示: 3.属性的懒加载(lazy) 在Swift的类中在对类进行初始化时,要对一些属性进行初始化。如果某些属性的初始化如果非常的耗费时间,那么在这种情况下我们就可以该初始化耗时的属性声明为懒加载的属性。就是在该属性声明的时候加上lazy关键字。被Lazy关键字修饰的变量会在使用时才会进行空间的分配。下方就是一个lazy的实例。 在下方实例中,除了MyPoint类,我们还需要定义一个MyCycle类。在MyCycle类中,使用到MyPoint类。在MyCycle类中的MyPoint属性为懒加载属性,具体请看代码,如下所示: (1) 定义MyCycle类,在MyCycle类中,定义一个属性为lazy的MyPoint类变量。如下所示: (2)接下来就是使用MyCycle, 声明MyCycle类型的变量,并为其分配MyCycle的类型实例。由下方实例可知,在调用MyCycle()构造函数时,MyCycle类中的point属性并没有对其进行初始化,此刻的point为nil。这样就减少了MyCycle初始化的时间。 (3) lazy属性point会在MyCycle实例对象在使用point属性时才会对其进行初始化,下方是myCycle实例变量调用point属性的代码片段,这时就明确的看到point是不为nil的。如下所示: 4. 计算属性(Count Property) 计算属性这一个特性在Objective-C中也是没有的。什么是计算属性呢,一句话概括:计算属性的值可以由其他属性的值来计算得到,同时在给计算属性赋值时也可以用来计算其他属性的值。也许说起来比较拗口,理解起来也许回有些困难,那么接下来来个小实例即可明白计算属性是怎么回事了。 下方我们创建一个名为Money的类,在Money类中有两个属性,一个是存储属性(普通属性)名为CNY(代表着人民币), 另一个是名为USD的计算属性(代表美元)。在USD计算属性的set方法中由USD的值计算CNY的值,在USD计算属性的get方法中由CNY计算出USD的值,并返回。Money类的具体代码片段如下所示: 计算属性在使用时和存储属性没有什么区别,下方是Money实例来调用其存储属性和计算属性的代码段,已经结果输出如下所示。下方代码段虽然简单,但是你慢慢的去品还是很有味道的。先看第一部分,也就是第一次给USD赋值,当给USD赋值时,CNY的之会立即被计算出来。 而当我们给CNY赋值时,USD的值不会被立即计算出来,因为只有在使用USD时才会调用get方法,这时候才会根据CNY的值来计算USD的值。具体结果请看下方代码段: 5. 属性观察 属性观察是用来干嘛的呢?说白了,属性观测器就是来观察属性的赋值情况的,属性观测器包括willSet()和didSet , willSet在属性将要被赋值的时候被调用, didSet是在属性被赋值后调用,关于这两个属性观察函数,写个实例就一目了然了。由下方实例可知,在willSet调用时,property属性的值还为默认值,但是在didSet执行时,property的值已经成为被赋予的值了。 6. 实例方法与类方法 在Objc中,类方法是由+来修饰的,实例方法是由-号来修饰的。在Swift的方法中就没有+或者-号进行修饰了,但是Swift中声明方法时,多了一个class。普通方法没有什么特别之处,而类方法的声明和定义需要在关键字func前添加class关键字。下方MyTestClass中定义了一个实例方法和一个类方法,并且给出了调用方式,如下所示: 今天博客的内容就先到这儿,下篇博客会涉及一些类的继承和类中的方法和属性的访问权限等其他一些关于类的东西。
上一篇博客《窥探Swift之别具一格的Struct和Class》的博客可谓是给Swift中的类开了个头。关于类的内容还有很多,今天就来搞一下类中的继承以及类的访问权限。说到类的继承,接触过面向对象编程(OOP)的小伙伴并不陌生,继承就是OOP编程中几大特征之一,所以还是有必要把类的继承拎出来聊聊的。说到访问权限,这个在OOP编程中也是不可或缺的。如果你接触过其他OOP的语言,你应该对private, public, protected并不陌生。在Swift这么面向对象的编程语言中,也有类似的概念,不过其具体表达的方式以及每种权限的作用域不同罢了。在Swift中的访问权限有private, internal与public。他们的作用域与其他语言还是有些区别的,这个稍后会细细的道来。 今天的博客在类的内容中还是比较重要的,同时也是比较基础的东西,当然很有必要好好的搞一下喽。在介绍继承和访问权限时,在恰当的地方会类比一下Objc, 如果你没有接触过Objc, 那么没关系,你可以把类比的部分给忽略掉,这并不影响你对Swift相应内容的理解。好~开始今天博客的内容。 一. 类的继承 其实继承理解起来还是蛮简单的,说白了就是子承父业。子类可以继承父类的一些东西,在继承之时,父类可以选择一些东西留给子类,同时也可以保留一些东西作为私有(private)物品。同样,子类也可以选择一些东西进行继承,如果对继承的东西不太满意,子类就可以对继承过来的东西进行重新改造(override)以满足自己的需求。如果要定义抽象类,只需把该类的构造器定义为私有即可。下方将会给出类的继承的一些示例,通过这些事例来认识一下Swift中的继承。 1. 父类的创建 这里我们创建一个名为Father的父类,他类可以继承Father类。在Father类中有两个初始值为空串的属性变量,并且有一个含有默认值参数的构造器,还有一个是输出变量值的displayAllName()的方法。类中的代码比较简单,具体如下所示。 import Foundation class Father { var surname: String = "" var name:String = "" init(surname:String = "", name:String = "") { self.surname = surname self.name = name } func displayAllName() { print("我叫\(self.surname)\(self.name)") } } 如果要定义抽象类,只需把上面的构造器进行一个修改即可。抽象类即为专门用来做其他类的父类的类,抽象类不能够直接被实例化,所以把其构造器定义为私有的就可以达到不能直接被实例化的目的。如果要把上述类修改成抽象类,那么把上面的构造函数替换成下方私有的构造函数即可。 1 private init() { 2 self.surname = "" 3 self.name = "" 4 } 2.实现Father的子类Children Children类继承自Father类,并在Father类的基础上做了相应的扩充。Children中的构造器调用了父类的构造器来对父类的一些属性进行初始化,当然,你也可以直接使用self来访问父类的非私有属性进行初始化,因为Children是继承自Father的,所以Father中非私有的东西都是视为己有的所以可以使用self来访问父类非私有的东西。子类持有了父类的非私有的东西,同时还可以增加属于自己的东西,子类肯定是要在父类的基础上在添加一些属于自己特有的东西。Children添加了父类中没有的job和disPlayMyJob的方法。 class Children: Father { var job:String = "" init(surname:String = "", name:String = "", job:String = "") { super.init(surname: surname, name: name) self.job = job } func displayMyJob() { print("我的工作是\(self.job)") } } 3.Children类的实例化 Chidren就是个类,其使用方式没有什么特别之处,就是调用构造器来进行类的实例化。实例化后,就可以调用类的非私有的属性和方法了,具体代码如下: 1 let children = Children(surname:"li", name:"zelu", job:"Coder") 2 children.displayAllName() 3 children.displayMyJob() 4.防止类的子类化 抽象类的使命是专门用来继承的,而有些类是不想让其他类进行继承的(丁克家族)。举个例子,有些小夫妻呢,就不想要孩子,那么怎么办呢?在Swift中也是有final关键字的,被final关键字所修饰的类是不能用来继承的。我们可以把Father类定义成final类型,并观察Children发生的变化。下方是修改后Father类的代码: final class Father { …………………… …………………… } Father添加上final关键字修饰后如果Children还对其进行继承那么就会报下面的错误,如下所示: 二、访问权限 Swift中的访问权限与其他面向对象编程语言有所不同,虽然Swift中的访问权限也分为3个等级,但是每个等级所能访问的区域与其他编程语言相比还是有些区别的。在Swift的访问权限包括public, private, internal三种访问级别。下面将要对这三个级别一一进行介绍。 public: 公有访问权限,类或者类的公有属性或者公有方法可以从文件或者模块的任何地方进行访问。那么什么样才能成为一个模块呢?一个App就是一个模块,一个第三方API, 第三等方框架等都是一个完整的模块,这些模块如果要对外留有访问的属性或者方法,就应该使用public的访问权限。 private: 私有访问权限,被private修饰的类或者类的属性或方法可以在同一个物理文件中访问。如果超出该物理文件,那么有着private访问权限的属性和方法就不能被访问。 internal: 顾名思义,internal是内部的意思,即有着internal访问权限的属性和方法说明在模块内部可以访问,超出模块内部就不可被访问了。在Swift中默认就是internal的访问权限。 关于访问权限的实例就不过赘述了,理解起来还算是简单的,今天的博客就先到这儿,接下来回继续更新关于Swift相关的博客。如有错误还望批评指正。
协议与委托代理回调在之前的博客中也是经常提到和用到的在《Objective-C中的委托(代理)模式》和《iOS开发之窥探UICollectionViewController(四) --一款功能强大的自定义瀑布流》等博客内容中都用到的Delegate回调。说到协议,在Objective-C中也是有协议的,并且Swift中的协议和Objc中的协议使用起来也是大同小异的,在Java等现代面向对象编程语言中有接口(Interface)的概念,其实和Swift中或者Objc中的Protocol(协议)是一个东西。论Interface和Protocol的功能来说,两者也是大同小异的。 今天就结合两个实例来窥探一下Swift中的协议与Delegate回调(委托代理回调)。本篇先给出CocoaTouch中常用控件UITableView的常用回调,并以此来认识一下回调的使用方式。紧接着会给出如何去实现自己的Delegate回调,即在自定义控件中去实现委托代理回调。言归正传,开始今天的博客主题。 一.从UITableView中来窥探协议的委托代理回调 UITableView这个高级控件在iOS开发中的出镜率是比较高的,今天的重点不是介绍如何使用UITableView, 而是让通过UITableView的工作方式来直观的感受一下协议的使用场景,以及Delegate代理的工作方式。如果你对UITableView控件不熟的话,完全可以跳过这一部分,直接进入第二部分。如果你要更好的理解Delegate委托回调,还是很有必要看这一部分的。 下面就先以UITableView的UITableViewDatasource协议来看一下委托代理的使用方式。为了简化代码呢,下面的TableView的使用就没有实现UITableViewDelegate协议还是那句话,今天的重点是Protocol和Delegate, 而不是如何使用UITableView。下方的截图就是我们要使用UITableView和UITableViewDatasource来做的事情。当然下方的实例无论是代码还是布局方面还是灰常简单的,运行效果如下所示。 上面的Cell中就是一个ImageView和一个Label, 布局灰常简单啦,接下来就简单介绍一下在Swift中是如何实现(说白了,和Objc实现起来大同小异)。还是结合着Storyboard来做吧,毕竟使用Storyboard布局更为简单一些。 1. 使用Storyboard来布局控件,控件布局如下: 2. 给上述Cell绑定相应的Swift源码,并关联ImageView和Label, 相应Cell(BeautifulGrillCell)的代码如下所示。girlImageView即为做吧的图片, girlNameLable为图片右边的文字。 import UIKit class BeautifulGrillCell: UITableViewCell { @IBOutlet var girlImageView: UIImageView! @IBOutlet var girlNameLable: UILabel! override func awakeFromNib() { super.awakeFromNib() // Initialization code } override func setSelected(selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) // Configure the view for the selected state } } 3.接下来就是要模拟我们在TableView上显示的数据了,在正常开放中这些数据往往来源于网络请求,而在本篇博客中就模拟数据源,来为我们的TableView提供显示的数据。数据源的格式是一个数组,而数组中存放的是多个字典,每个字典有两个键值对,一个键值对存储要显示图片的文件名,另一个键值对则存储美女的名字。为了使该数据的存储结构,请看下方结构图。 原理图有了,接下来就要使用代码来创建出上述结构的数据以供TableView的数据源使用,下面的方法就是实现上述结构的函数。 (1) 首先我们要在视图控制器相应的类中添加一个可变数组,用来存放数据,如下所示: 1 private var dataSource:Array<Dictionary<String, String>>? (2) 接着就是往上面这个数组中填充数据了,代码如下: //-----------创建Table要显示的数据------------------------- func createSourceData() { self.dataSource = Array<Dictionary<String, String>>(); for (var i = 0; i<10; i++) { let imageName:String = "00\(i).jpg" let girlName:String = "美女\(i + 1)" self.dataSource?.append([IMAGE_NAME:imageName, GIRL_NAME:girlName]) } } 4. 我们上面Storyboard中的视图控制器使用的是UIViewController而不是UITableViewController。 我们在UIViewController上贴了一层UITableView, 所以我们需要在相应的ViewController对应的Swift源码中进行UITableView的绑定,并实现UITableViewDatasource代理,并为UITableView指定该代理。下方的代码就是关联tableview并指定代理方法。代码如下: import UIKit class ViewController: UIViewController, UITableViewDataSource { @IBOutlet var myTableView: UITableView! //life cycle override func viewDidLoad() { super.viewDidLoad() self.createSourceData() self.myTableView.dataSource = self } } 4. 对myTableView的dataSource(数据提供者)指定完代理对象后,接下来就是要实现UITableViewDataSource中的相应的方法了,ViewController通过这些协议委托回调的代理方法来为TableView提供数据。下方是UITableViewDataSource委托方法中返回TableView的Section个数的回调方法,如下所示: /** - parameter tableView: 当前要显示的TableView - returns: TableView中Section的个数 */ func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 18 } 5.上面回调方法是返回Section个数的,紧接着下方就是返回每个Section中Cell个数的回调方法。Cell的个数就是数组dataSource中元素的个数。 /** 返回每个Section中的Cell个数 - parameter tableView: 当前显示的TableView - parameter section: 对应的Section - returns: 对应Section中cell的个数 */ func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return self.dataSource!.count } 6. 下面这个方法是比较重要的,下方的方法,就是返回每行的Cell的委托回调方法。通过Cell的重用标示符来创建Cell的实例对象,并对Cell上的一些属性赋值,并返回当前是Cell实例对象,代码如下所示。 /** 返回要显示的Cell - parameter tableView: cell要显示的TableView - parameter indexPath: cell的索引信息 - returns: 返回要显示的Cell对象 */ func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell:BeautifulGrillCell = self.myTableView.dequeueReusableCellWithIdentifier("BeautifulGrillCell", forIndexPath: indexPath) as! BeautifulGrillCell let tempItem:Dictionary? = self.dataSource![indexPath.row] if tempItem != nil { let imageName:String = tempItem![IMAGE_NAME]! cell.girlImageView.image = UIImage(named: imageName) let girlName:String = tempItem![GIRL_NAME]! cell.girlNameLable.text = girlName } return cell } } 经过上面这些步骤,你就可以去实现博客最上方截图中的效果了,上面主要用到的还是TableView的UITableViewDatasource委托代理, 使用方法如上。上面使用的委托回调主要是使用Swift中的协议(Protocol)来实现的。那么如何使用协议来实现你自己的委托回调呢?这将是下面将要介绍的内容。 二. 认识协议,并使用协议实现委托回调 接下来的内容就要介绍如何使用协议来定义属于你自己的委托代理回调(Delegate)了。第二部分还是以实例为准,在上面的Demo中加入我们自己定义的委托代理回调。我们需要做的就是,在上面界面中,我们点击任意Cell就可以Push(导航控制器展示视图控制器的一种方式,可以理解为视图控制器压栈的过程)到一个ViewController中,这个ViewController要做的事情就是输入美女的名字,点击返回后通过自己定义的委托回调,把你输入的值回调到上一个页面(TableView)中去,并修改相应Cell上的名字。说白了,就是对美女的名字做一个修改。 如果上面的文字让你迷惑的话,那么接下来看实例好了,该实例还算是简单的。下方是实例的操作步骤,如下所示: 上面实例的意思就是把下一个页面的值通过委托代理回调的形式传到上个页面中去,在前面的博客《窥探Swift之函数与闭包的应用实例》中也做了同样的事情,不过之前我们是使用闭包(Closure)回调来实现的。先在我们要通过Delegate来实现。接下来我们就定义协议,然后再协议的基础上实现委托代理回调。接下来了开始我扩充的部分。 1.实现编辑美女姓名的页面 (1) 在Storyboard上新添加一个视图控制器(UIViewController), 并命名为EditViewController,给视图控制器就是上方截图中绿色的那个视图控制器,主要用来对美女姓名 修改,并通过委托回调把值传给上个页面。该视图控制器的页面布局比较简单,具体如下所示: (2)UI就如数所示,为EditViewController关联EditViewController.swift源文件后,再对其上面的使用到的控件进行关联即可。紧接着我们要实现一个协议,这个协议我们用来所委托回调使用。这个协议可以定义在EditViewController.swift源文件中。在协议定义之前,先对什么是协议简单的提上一嘴。先简单的理解,协议中的方法只有声明,没有实现,并且使用protocol关键自进行声明,下方的代码就是我们要使用的协议。协议中有一个fetchGirlName(name:String)的方法,用来回调出输入的数值。默认方法是必选的,你可以使用optional关键字使方法可选,在此就不做过多赘述了。 1 protocol EditViewControllerDelegate: NSObjectProtocol{ 2 func fetchGirlName(name:String) 3 } (3) 接着要实现EditViewController类中的东西了,代码如下。 成员变量var girlOldName:String?负责接收上个页面传过来的美女的姓名。weak var delegate:EditViewControllerDelegate? 这个声明为weak的delegate成员变量则是必须要实现EditViewControllerDelegate协议的委托代理者,使用weak修饰为了避免强引用循环。接着是girlNameTextField就是关联的输入框了,负责接收用户输入,把值交付给委托代理者。 在viewWillDisappear方法中,会将用户输入的值交付给委托代理者的fetchGirlName方法。deinit是析构函数,用来观察是否引起强引用循环,因为我们是使用的weak, 所以不会引起强引用循环,该deinit方法当返回时,是会被释放掉的。 class EditViewController: UIViewController { var girlOldName:String? weak var delegate: EditViewControllerDelegate? @IBOutlet var girlNameTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() if self.girlOldName != nil { self.girlNameTextField.text = self.girlOldName! } } override func viewWillDisappear(animated: Bool) { let name:String! = self.girlNameTextField.text if name != "" { if delegate != nil { delegate!.fetchGirlName(name) } } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } deinit { print("释放") } } 2.上面的代码是实现编辑页面并实现相应的委托协议,下方就是要从之前TableView中进行跳转。也就是点击TableView的每一行,然后跳转到编辑页面对其当前点击的cell进行编辑,编辑后返回通过代理进行值的修改。 (1)首先要解决的就是点击Cell跳转到EditViewController, 要执行这个事件,我们还必须实现TableView的另一个协议,就是UITableViewDelegate, 以为点击Cell的事件获取的方法就在TableViewDelegate中。所以我们要在TableView所在的ViewController中的viewDidLoad()中指定UITableViewDelegate的委托代理者。如下所示。同时该ViewContoller也要实现UITableViewDelegate协议。 1 self.myTableView.delegate = self (2) 实现UITableViewDelegate协议中点击Cell的方法,方法中的内容如下所示。在该方法中,首先我们要暂存一下点击的是哪个Cell, 也就是记录一下点击Cell的IndexPath, 然后就是获取点击的Cell对象,因为通过该Cell对象,可以获取相应Cell上的数据。具体的不多说了,请看代码中的注释。 //-----------UITableViewDelegate------------------ func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { //记录当前点击的IndexPath self.selectIndexPath = indexPath //获取当前点击的Cell对象 let currentSelectCell:BeautifulGrillCell? = self.myTableView.cellForRowAtIndexPath(indexPath) as? BeautifulGrillCell //从storyboard中实例化编辑视图控制器 let editViewController:EditViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("EditViewController") as! EditViewController //指定编辑视图控制器委托代理对象 editViewController.delegate = self //把点击Cell上的值传递给编辑视图控制器 if currentSelectCell != nil { editViewController.girlOldName = currentSelectCell!.girlNameLable.text! } //push到编辑视图控制器 self.navigationController?.pushViewController(editViewController, animated: true) } (3)上面是跳转,接下来就是要实现EditViewControllerDelegate中的回调方法,来处理相应的回调参数了。下方就是在表视图中实现的回调方法,具体请看代码中的注释: //-----------EditViewControllerDelegate------------------ func fetchGirlName(name: String) { if selectIndexPath != nil { //获取当前点击Cell的索引 let index = (selectIndexPath?.row)! //更新数据源中相应的数据 self.dataSource![index][GIRL_NAME] = name //重载TableView self.myTableView.reloadData() } } 经过上面的步骤,我们就可以去定义属于自己的协议,并在此协议上实现委托回调了。上面的场景在iOS开发中极为常见,使用场景也是比较广泛的。所以协议无论在Swift还是在iOS开发中都是极为重要的概念之一。好今天的博客内容也挺多的了,就到此为止,剩下的东西,会在以后的博客中继续更新。
协议与委托代理回调在之前的博客中也是经常提到和用到的在《Objective-C中的委托(代理)模式》和《iOS开发之窥探UICollectionViewController(四) --一款功能强大的自定义瀑布流》等博客内容中都用到的Delegate回调。说到协议,在Objective-C中也是有协议的,并且Swift中的协议和Objc中的协议使用起来也是大同小异的,在Java等现代面向对象编程语言中有接口(Interface)的概念,其实和Swift中或者Objc中的Protocol(协议)是一个东西。论Interface和Protocol的功能来说,两者也是大同小异的。 今天就结合两个实例来窥探一下Swift中的协议与Delegate回调(委托代理回调)。本篇先给出CocoaTouch中常用控件UITableView的常用回调,并以此来认识一下回调的使用方式。紧接着会给出如何去实现自己的Delegate回调,即在自定义控件中去实现委托代理回调。言归正传,开始今天的博客主题。 一.从UITableView中来窥探协议的委托代理回调 UITableView这个高级控件在iOS开发中的出镜率是比较高的,今天的重点不是介绍如何使用UITableView, 而是让通过UITableView的工作方式来直观的感受一下协议的使用场景,以及Delegate代理的工作方式。如果你对UITableView控件不熟的话,完全可以跳过这一部分,直接进入第二部分。如果你要更好的理解Delegate委托回调,还是很有必要看这一部分的。 下面就先以UITableView的UITableViewDatasource协议来看一下委托代理的使用方式。为了简化代码呢,下面的TableView的使用就没有实现UITableViewDelegate协议还是那句话,今天的重点是Protocol和Delegate, 而不是如何使用UITableView。下方的截图就是我们要使用UITableView和UITableViewDatasource来做的事情。当然下方的实例无论是代码还是布局方面还是灰常简单的,运行效果如下所示。 上面的Cell中就是一个ImageView和一个Label, 布局灰常简单啦,接下来就简单介绍一下在Swift中是如何实现(说白了,和Objc实现起来大同小异)。还是结合着Storyboard来做吧,毕竟使用Storyboard布局更为简单一些。 1. 使用Storyboard来布局控件,控件布局如下: 2. 给上述Cell绑定相应的Swift源码,并关联ImageView和Label, 相应Cell(BeautifulGrillCell)的代码如下所示。girlImageView即为做吧的图片, girlNameLable为图片右边的文字。 import UIKit class BeautifulGrillCell: UITableViewCell { @IBOutlet var girlImageView: UIImageView! @IBOutlet var girlNameLable: UILabel! override func awakeFromNib() { super.awakeFromNib() // Initialization code } override func setSelected(selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) // Configure the view for the selected state } } 3.接下来就是要模拟我们在TableView上显示的数据了,在正常开放中这些数据往往来源于网络请求,而在本篇博客中就模拟数据源,来为我们的TableView提供显示的数据。数据源的格式是一个数组,而数组中存放的是多个字典,每个字典有两个键值对,一个键值对存储要显示图片的文件名,另一个键值对则存储美女的名字。为了使该数据的存储结构,请看下方结构图。 原理图有了,接下来就要使用代码来创建出上述结构的数据以供TableView的数据源使用,下面的方法就是实现上述结构的函数。 (1) 首先我们要在视图控制器相应的类中添加一个可变数组,用来存放数据,如下所示: 1 private var dataSource:Array<Dictionary<String, String>>? (2) 接着就是往上面这个数组中填充数据了,代码如下: //-----------创建Table要显示的数据------------------------- func createSourceData() { self.dataSource = Array<Dictionary<String, String>>(); for (var i = 0; i<10; i++) { let imageName:String = "00\(i).jpg" let girlName:String = "美女\(i + 1)" self.dataSource?.append([IMAGE_NAME:imageName, GIRL_NAME:girlName]) } } 4. 我们上面Storyboard中的视图控制器使用的是UIViewController而不是UITableViewController。 我们在UIViewController上贴了一层UITableView, 所以我们需要在相应的ViewController对应的Swift源码中进行UITableView的绑定,并实现UITableViewDatasource代理,并为UITableView指定该代理。下方的代码就是关联tableview并指定代理方法。代码如下: import UIKit class ViewController: UIViewController, UITableViewDataSource { @IBOutlet var myTableView: UITableView! //life cycle override func viewDidLoad() { super.viewDidLoad() self.createSourceData() self.myTableView.dataSource = self } } 4. 对myTableView的dataSource(数据提供者)指定完代理对象后,接下来就是要实现UITableViewDataSource中的相应的方法了,ViewController通过这些协议委托回调的代理方法来为TableView提供数据。下方是UITableViewDataSource委托方法中返回TableView的Section个数的回调方法,如下所示: /** - parameter tableView: 当前要显示的TableView - returns: TableView中Section的个数 */ func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 18 } 5.上面回调方法是返回Section个数的,紧接着下方就是返回每个Section中Cell个数的回调方法。Cell的个数就是数组dataSource中元素的个数。 /** 返回每个Section中的Cell个数 - parameter tableView: 当前显示的TableView - parameter section: 对应的Section - returns: 对应Section中cell的个数 */ func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return self.dataSource!.count } 6. 下面这个方法是比较重要的,下方的方法,就是返回每行的Cell的委托回调方法。通过Cell的重用标示符来创建Cell的实例对象,并对Cell上的一些属性赋值,并返回当前是Cell实例对象,代码如下所示。 /** 返回要显示的Cell - parameter tableView: cell要显示的TableView - parameter indexPath: cell的索引信息 - returns: 返回要显示的Cell对象 */ func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell:BeautifulGrillCell = self.myTableView.dequeueReusableCellWithIdentifier("BeautifulGrillCell", forIndexPath: indexPath) as! BeautifulGrillCell let tempItem:Dictionary? = self.dataSource![indexPath.row] if tempItem != nil { let imageName:String = tempItem![IMAGE_NAME]! cell.girlImageView.image = UIImage(named: imageName) let girlName:String = tempItem![GIRL_NAME]! cell.girlNameLable.text = girlName } return cell } } 经过上面这些步骤,你就可以去实现博客最上方截图中的效果了,上面主要用到的还是TableView的UITableViewDatasource委托代理, 使用方法如上。上面使用的委托回调主要是使用Swift中的协议(Protocol)来实现的。那么如何使用协议来实现你自己的委托回调呢?这将是下面将要介绍的内容。 二. 认识协议,并使用协议实现委托回调 接下来的内容就要介绍如何使用协议来定义属于你自己的委托代理回调(Delegate)了。第二部分还是以实例为准,在上面的Demo中加入我们自己定义的委托代理回调。我们需要做的就是,在上面界面中,我们点击任意Cell就可以Push(导航控制器展示视图控制器的一种方式,可以理解为视图控制器压栈的过程)到一个ViewController中,这个ViewController要做的事情就是输入美女的名字,点击返回后通过自己定义的委托回调,把你输入的值回调到上一个页面(TableView)中去,并修改相应Cell上的名字。说白了,就是对美女的名字做一个修改。 如果上面的文字让你迷惑的话,那么接下来看实例好了,该实例还算是简单的。下方是实例的操作步骤,如下所示: 上面实例的意思就是把下一个页面的值通过委托代理回调的形式传到上个页面中去,在前面的博客《窥探Swift之函数与闭包的应用实例》中也做了同样的事情,不过之前我们是使用闭包(Closure)回调来实现的。先在我们要通过Delegate来实现。接下来我们就定义协议,然后再协议的基础上实现委托代理回调。接下来了开始我扩充的部分。 1.实现编辑美女姓名的页面 (1) 在Storyboard上新添加一个视图控制器(UIViewController), 并命名为EditViewController,给视图控制器就是上方截图中绿色的那个视图控制器,主要用来对美女姓名 修改,并通过委托回调把值传给上个页面。该视图控制器的页面布局比较简单,具体如下所示: (2)UI就如数所示,为EditViewController关联EditViewController.swift源文件后,再对其上面的使用到的控件进行关联即可。紧接着我们要实现一个协议,这个协议我们用来所委托回调使用。这个协议可以定义在EditViewController.swift源文件中。在协议定义之前,先对什么是协议简单的提上一嘴。先简单的理解,协议中的方法只有声明,没有实现,并且使用protocol关键自进行声明,下方的代码就是我们要使用的协议。协议中有一个fetchGirlName(name:String)的方法,用来回调出输入的数值。默认方法是必选的,你可以使用optional关键字使方法可选,在此就不做过多赘述了。 1 protocol EditViewControllerDelegate: NSObjectProtocol{ 2 func fetchGirlName(name:String) 3 } (3) 接着要实现EditViewController类中的东西了,代码如下。 成员变量var girlOldName:String?负责接收上个页面传过来的美女的姓名。weak var delegate:EditViewControllerDelegate? 这个声明为weak的delegate成员变量则是必须要实现EditViewControllerDelegate协议的委托代理者,使用weak修饰为了避免强引用循环。接着是girlNameTextField就是关联的输入框了,负责接收用户输入,把值交付给委托代理者。 在viewWillDisappear方法中,会将用户输入的值交付给委托代理者的fetchGirlName方法。deinit是析构函数,用来观察是否引起强引用循环,因为我们是使用的weak, 所以不会引起强引用循环,该deinit方法当返回时,是会被释放掉的。 class EditViewController: UIViewController { var girlOldName:String? weak var delegate: EditViewControllerDelegate? @IBOutlet var girlNameTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() if self.girlOldName != nil { self.girlNameTextField.text = self.girlOldName! } } override func viewWillDisappear(animated: Bool) { let name:String! = self.girlNameTextField.text if name != "" { if delegate != nil { delegate!.fetchGirlName(name) } } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } deinit { print("释放") } } 2.上面的代码是实现编辑页面并实现相应的委托协议,下方就是要从之前TableView中进行跳转。也就是点击TableView的每一行,然后跳转到编辑页面对其当前点击的cell进行编辑,编辑后返回通过代理进行值的修改。 (1)首先要解决的就是点击Cell跳转到EditViewController, 要执行这个事件,我们还必须实现TableView的另一个协议,就是UITableViewDelegate, 以为点击Cell的事件获取的方法就在TableViewDelegate中。所以我们要在TableView所在的ViewController中的viewDidLoad()中指定UITableViewDelegate的委托代理者。如下所示。同时该ViewContoller也要实现UITableViewDelegate协议。 1 self.myTableView.delegate = self (2) 实现UITableViewDelegate协议中点击Cell的方法,方法中的内容如下所示。在该方法中,首先我们要暂存一下点击的是哪个Cell, 也就是记录一下点击Cell的IndexPath, 然后就是获取点击的Cell对象,因为通过该Cell对象,可以获取相应Cell上的数据。具体的不多说了,请看代码中的注释。 //-----------UITableViewDelegate------------------ func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { //记录当前点击的IndexPath self.selectIndexPath = indexPath //获取当前点击的Cell对象 let currentSelectCell:BeautifulGrillCell? = self.myTableView.cellForRowAtIndexPath(indexPath) as? BeautifulGrillCell //从storyboard中实例化编辑视图控制器 let editViewController:EditViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("EditViewController") as! EditViewController //指定编辑视图控制器委托代理对象 editViewController.delegate = self //把点击Cell上的值传递给编辑视图控制器 if currentSelectCell != nil { editViewController.girlOldName = currentSelectCell!.girlNameLable.text! } //push到编辑视图控制器 self.navigationController?.pushViewController(editViewController, animated: true) } (3)上面是跳转,接下来就是要实现EditViewControllerDelegate中的回调方法,来处理相应的回调参数了。下方就是在表视图中实现的回调方法,具体请看代码中的注释: //-----------EditViewControllerDelegate------------------ func fetchGirlName(name: String) { if selectIndexPath != nil { //获取当前点击Cell的索引 let index = (selectIndexPath?.row)! //更新数据源中相应的数据 self.dataSource![index][GIRL_NAME] = name //重载TableView self.myTableView.reloadData() } } 经过上面的步骤,我们就可以去定义属于自己的协议,并在此协议上实现委托回调了。上面的场景在iOS开发中极为常见,使用场景也是比较广泛的。所以协议无论在Swift还是在iOS开发中都是极为重要的概念之一。好今天的博客内容也挺多的了,就到此为止,剩下的东西,会在以后的博客中继续更新。
有的小伙伴会问:博主,没有Mac怎么学Swift语言呢,我想学Swift,但前提得买个Mac。非也,非也。如果你想了解或者初步学习Swift语言的话,你可以登录这个网站:http://swiftstub.com/ 。该网站可以在线运行出代码结果,也可以说这是一个在线的Playground。你可以实时观察你代码的运行结果。如果你没有Mac笔记本,那么你只需打开你的浏览器,然后输入上述网址,就可以搞搞Swift这门语言了,灰常好用的呢。下方的截图就是该网址打开的截图。 上面如果算是工具性的网站的话,那么接下来将会给大家介绍一个学习网站:http://swiftdoc.org 。该网站集成了Swift的各种东西,内容虽然是英文的,但是里边的东西还是相当不错的,里边的内容也会随着Swift语言版本的更新而更新。如果你的英文比较好,那么完全可以去这个网站中去汲取你的知识。下方是该网站的一个截图,也是一个比较好的学习的地方。 Swift中也是支持泛型的,在许多现代编程语言,如C++, Java, C#也都是支持泛型的。泛型,从表面的名字来看,就是宽泛的数据类型。使用泛型定义的方法,类,结构体,协议等可以支持不同的数据类型。泛型其实就是数据类型的占位符。当然这个占位符的名字有你来定,你定义的这个占位符就是数据类型的变量,你传给他什么类型,那么这个泛型占位符就代表什么类型。这样说来泛型理解起来就不困难了。 今天博客中的内容算是比较简单,也是比较基础,虽简单,但失其重要性。今天博客中就通过一些示例来窥探一下泛型的使用方法和使用场景。无论你是在函数,类,协议,延展等场景中使用泛型。他们有一个共性,同时也是泛型的特点“高度重用性”。能写出高度重用的东西,在编程中是灰常令人兴奋的一件事情。 一. 泛型函数 1.单一占位符泛型函数 下面就使用一个经典案例:两个数值进行交换。来使用泛型,写一个通用的函数,这个函数的功能就是交换两个变量的值。在Swift中不允许类型隐式转换,也就是说,如果你定义的该函数是交换两个整数的,那么如果你想使用他来交换浮点类型的数据,那么对不起,是不允许这样做的。为了写个通用的函数,那接下来就是泛型出场的时候了。 下面就是使用泛型来定义一个交换两个变量的值的函数,该函数如下图所示。其中MyCustomeType就是在函数中定义的泛型占位符,改占位符表示传入的参数是什么类型,那么MyCustomeType就是什么类型。这样一来,使用泛型定义的该函数就是通用的了。在该函数中只用到了一个MyCustomeType,也就是下方的函数还是有一定约束性,就是number1和number2的类型都是相同数据类型的,这种情况也是单一占位符泛型函数。 调用上述函数来交换两个字符串类型的值: 同样的函数,你还可以使用它来交换两个整数,浮点数等等其他数据类型,下方就是交换的两个整型的数据,也是没有问题的。 2.多个占位符的泛型函数 这个是在上述函数中进行的扩充,泛型占位符允许是一个列表的形式出现的,也就是允许有多个不同的泛型占位符来代表不同的数据类型。也许说起来有些拗口,接下来就来个实例在函数中使用多个泛型占位符。具体实例如下所示。本质上允许在泛型占位符中添加多个泛型类型变量。下方的函数两个参数的数据类型允许不同,因为其参数使用的是不同的泛型占位符,所以其类型允许是不同的。具体使用方式如下所示。 二.泛型类 泛型类,顾名思义,就是在类中使用泛型。在类中使用泛型,其实和函数中使用泛型是一样的。就是在声明类的时候,使用泛型占位符表示一个要处理的泛型即可。下方就是一个泛型类,其中有个泛型类型数组,还有一个打印该数组的方法,如下所示: 上面是泛型类的定义,紧接着就是泛型类的使用了,下方是创建一个泛型类的实例,然后调用相应的方法,具体如下所示。 上面是泛型类的定义与使用,泛型还可以应用于结构体,协议,延展等,其使用方法和泛型类是差不多的,要学会举一反三。在本篇博客中就不对泛型结构体,泛型协议,泛型延展,以及泛型的约束做过多的赘述了。
Swift到目前为止仍在更新,每次更新都会推陈出新,一些Swift旧版本中的东西在新Swift中并不适用,而且新版本的Swift会添加新的功能。到目前为止,Swift为2.1版本。去年翻译的Swift书籍是1.0版本,所以上面一些东西并不在适用。虽然Swift语言仍在更新,但是其整体的基础框架已经形成,大的改动应该不会有,版本的更新更多的是语言新功能的添加和完善,所以并不用担心现在学的Swift会过时。更新也就是在原有的基础上去更新,所以学学Swift还是很有必要的。新的Swift版本中引入了好多新的概念,比如if-let,guard,柯里化,自定义Quick Help等等。 关于iOS的内容请参见《我的iOS开发系列博文》,Object-C的内容请参见《我的Objective-C系列文章》。 一:Swift语言版本更新 之前陆陆续续的也发表过一些关于Swift语言的博客,由于Swift版本间的差异,所以之前博客中的某些代码,更确切的说是某些语法在最新的Xcode中会编译不过去的。不过不用担心Apple公司已经为我们考虑好了,在Xcode中就带有Swift语言版本更新的工具,该工具可以将代码更新到最新的Swift语言版本中。下方就是将你的Swift代码更新到最新Swift语言版本的步骤: 1. 点击 菜单-》Edit->Convert->To Latest Swift Syntax…,如下图所示: 2. 经过第一步,紧接着进入下方的提示,点击Next: 3. 选择要转换的目标,点击Next即可: 4.点击Next会进入转换状态,转换完就会进入对比状态,这一点和使用Xcode进行提交SVN特别像。你可以查看那些地方被转换过。具体效果如下:
在Swift中的数组和字典中下标是非常常见的,数组可以通过索引下标进行元素的查询,字典可以通过键下标来获取相应的值。在使用数组时,一个常见的致命错误就是数组越界。如果在你的应用程序中数组越界了,那么对不起,如果由着程序的性子的话是会崩溃的。为了防止崩溃呢,我们会对集合做一些安全的处理。比如对数组进行扩展,从而对数组的索引进行安全检查,保证数组的index在正常范围内。在Objective-C中也是经常对数组,字典等做一些处理操作。 今天的博客的主要内容是先对Objective-C中常用集合的安全扩展进行介绍,由此在窥探一下Swift语言中的处理。并且还会介绍Swift中自定义下标,说白了自定义下标其实就是通过下标的形式与特定的下标值来访问一个对象。自定义下标在有些场合中是非常实用的。然后下方还会给出数组切片的概念与实用方式。废话少说进入今天的主题。 一、安全的索引集合元素 对一个集合索引进行安全检查是很有必要的,也是经常实用的,最常见的就是对数组和字典索引的安全检查,该部分内容就是类比这Objective-C中的数组索引的安全检查来扩充Swift的数组,从而让你的Swift数组也同样具备对数组安全检查的功能。 1. Objective-C中NSArray对索引的安全扩展 下方这段代码是非常简单的,它是对Objective-C中的NSArray做的扩展,该方法位于NSArray相关的延展中。在你的项目中,如果添加了此段延展代码,那么你就可以通过objectAtIndexSafe:方法对数组进行安全的索引。有代码不难看出在定义该函数参数时,我们将index声明为NSUInteger,也就是正整数,这就排除了你对下标传入一个负数。紧接着又对index的合法性进行验证,如果index不在数组有效范围内,那么就返回nil。当你查找的元素不存在时,你返回nil是不会造成程序崩溃的,因为nil的地址是0x0, 这和归零若引用有些类似。 当然下方只是NSArray安全扩展其中一个方法,还有许多扩展的安全方法,比如数组的增删改查都可以进行相应的安全扩展,扩展的方式和思路与下方这段简单代码类似,再次就不花过多的篇幅对其进行介绍了。 - (id)objectAtIndexSafe:(NSUInteger)index { if (index > self.count-1) { return nil; } return [self objectAtIndex:index]; } 2.Swift中对Array的安全扩展 上面简单的对Objective-C中的安全方法进行了简单的介绍,就算是对Swift相关内容的引子吧,下方将会给出Swift语言中类似的方法。对Swift相关方法介绍时,我会尽量的详细一些,因为毕竟本篇博客主要是关于Swift内容的。接下来将对上面Objective-C中NSArray数组索引安全验证的方法使用Swift语言进行重新。当然重写的内容也是非常容易理解的。 (1)主要是对subscript方法进行重载,在重载的subscript方法中,对index的范围通过三目运算符进行了安全检查。如果index在0..<count这个半开区间内,那么就返回当前索引的值,如果不在该范围内就返回nil, 下方就是对Array索引的安全检查。 extension Array { subscript (safe index: Int) -> Element? { return (0..<count).contains(index) ? self[index] : nil } } (2)上面是对Swift中的Array进行了安全索引扩展,接下来就是简单的使用了,下方的代码段是对上面安全扩展函数的测试。首先创建了一个数组testArray, 然后创建了一个索引数组indexs, 然后遍历indexs中的元素值,将其作为testArray的下标,对testArray进行检索。当然检索时,使用的是我们上面定义的safe方法,并且在indexs下标数组中存在非法的下标。在这种情况下,我们来验证一下我们的安全方法。 当然在数组遍历中,我们使用了for-in循环取出indexs中的每个index, 然后使用guard语句取出testArray中的值。使用guard语句能很好的过滤掉因为非法的index而返回的nil值。具体代码段如下所示: 上面的代码段理解起来并不难,上述测试代码的运行结果如下所示,从运行结果可以很好的说明问题,并且在index非法时不会崩溃,并合理的给出相应的错误提示,请看下方具体运行结果。 上面的延展也可以通过对整个集合类型,也就是CollectionType进行扩展,不过在扩展CollectionType时要对Index使用where子句进行限制,使Index必须符合Comparable协议,具体实现如下所示,不过下面的方法比较少用,因为一般是数组存在越界的情况,因为在字典中,如果你对一个不存在的键进行值的索引,会返回nil值,而不会崩溃。但是在数组中,你对不存在的index进行索引,就会抛出错误。下方是另一种处理方式,不过该方式用的比较少。 实现下方延展后,同样可以在数组中使用safe方法。 二、使用多个索引下标的数组 延展的功能是非常强大的,该部分将会给出另一个数组的延展。该延展的功能是可以通过多个索引给数组设置值,以及通过多个索引一次性获取多个数组的值。该功能是非常强大的,接下来将一步步实现该功能。 1. 了解zip()函数以及Zip2Sequence 在实现数组多个索引扩展时,需要使用到zip()函数,zip()函数接收两个序列,并且返回一个Zip2Sequence类型的数据。zip()函数究竟是干嘛的呢?接下来将会通过一个小的实例来搞一下zip()函数。首先看一下Apple的帮助文档上对zip()函数的介绍。具体如下所示: 上面那句英文的意思大概就是“基于两个基本序列构建了一个序列对,在序列对中,第i对,代表着每个基本序列中的第i个元素。”在zip函数定义的过程中,我们可以看到,zip()是一个泛型函数,其接收两个SequenceType类型的参数,然后返回一个Zip2Sequence类型的数据。新创建的序列对就存在于Zip2Sequence中。说这么多还是来个小Demo实惠一些,通过一个小实例,看zip()函数的用法一目了然。 (1) 创建两个数组zip1和zip2, 将这两个数组作为zip()函数的参数,将两个数组进行合并。具体实现如下: (2) 通过上面的程序可以看出,zipSum是一个Zip2Sequence<Array<Int>, Array<Int>>类型的常量,我们可以使用dump()对zipSum常量进行打印,观察其中的数据存储结构,具体结构如下所示: 输出结果如下,由结果容易看出,在序列中有两个元素,第一个元素对应着数组zip1, 第二个元素对应着数组zip2。 (3)接下来就是对zipSum这个序列通过for-in循环进行遍历,下方就是对zipSum进行遍历的代码。 上面对zipSum遍历的结果如下所示,由下方输出结果可知,输出是成对遍历的,如果某个数组中的元素是多余的,那么就会被忽略掉。 2. 数组多个索引的延展实现 在这个将要实现的延展中,我们对Array进行了扩展,在延展中对subscript方法进行重载,使其可以接受多个下标,并且对多个下标对应的值进行索引,并把索引结果组成数组。在subscript方法中通过get方法获取索引相应的值,通过set方法为相应的索引值进行设置。下方代码段就是该延展的实现: extension Array { subscript(i1: Int, i2: Int, rest: Int...) -> [Element] { //通过实现get方法,获取数组中相应的值 get { var result: [Element] = [self[i1], self[i2]] for index in rest { result.append(self[index]) } return result } //通过set方法,对数组相应的索引进行设置 set (values) { for (index, value) in zip([i1, i2] + rest, values) { self[index] = value } } } } 在上述延展的实现中,并没有多少困难的地方。在subs两个cript函数中,使用的是可变参数,subscript函数参数的个数是两个以上(包括两个)。然后就是通过zip()函数以及对zip()函数返回的结果集进行遍历,从而对多个下标索引进行值的设置。经过上述延展,我们就可以通过多个索引对数组进行操作了。上述延展的使用方式如下: 三、数组切片 数组切片在OC中也是不存在的,是Swift新引入的概念,该部分将会对数组切片进行讨论,研究一下数组切片的使用方式及其特点。下方先通过一个小Demo来看一下如何生成数组切片。下方代码段先将一个字符串通过map函数转换成一个数组arrayTest, 然后我们创建一个该数组的切片。下方代码段创建了arrayTest数组中的下标3到下标6这个范围区间中的切片,arraySlices就是数组切片变量,它是ArraySlice<String>类型的,具体代码段如下所示。 在数组切片中有一点需要注意,数组切片的下标与原始数组中的下标保持一致。如果要取出切片arraySlices中的第一个值,我们要使用arraySlices[3], 而不是arraySlices[0], 如果使用arraySlices[0]就会报错,如下所示: 因为数组是值类型,尽管切片与原数组有着对应的数组下标,但是切片是原始数组的部分拷贝,所以修改切片或者修改原数组,两者互不影响,下方示例给出了该测试,如下所示: 如果把切片转换成枚举,那么切片中与原始数组对应的下标关系将不存在,下方是将切片转换成枚举序列,然后对其进行遍历,代码如下: 上述代码段输出结果如下: 今天博客就先写到这儿,关于数组的延展还有许多,以后有机会再讨论。其实我们还可以通过一些方式来为我们自己的对象添加下标。也就是可以通过下标来访问对象属性,这个以后在讨论吧。
在Swift 2.0版本中,Swift语言对其错误处理进行了新的设计,当然了,重新设计后的结果使得该错误处理系统用起来更爽。今天博客的主题就是系统的搞一下Swift中的错误处理,以及看一下Swift中是如何抛出异常的。在编译型语言中,错误一般分为编译错误和运行时错误。我们平时在代码中处理的错误为运行时错误,我们对异常进行处理的操作的目的是为了防止程序出现错误而导致其他的副作用,比如用户数据未保存等等。 在今天的博客中,先给出主动产生异常的几种情况,然后再给出如何处理被动异常。 一、主动退出程序的几种情况 在Objective-C中,在单元测试时我们会使用断言,断言中条件满足时会产生异常,并打印出相应的断言错误,在Swift中也有几种产生异常的语法。在本篇博客的第一部分就给出这几种方法。 1.Fatal Errors(致命的错误) 使用fatalError()函数可以立即终止你的应用程序,在fatalError()中可以给出终止信息。使用fatalError()函数,会毫无条件的终止你的应用程序,用起来也是比较简单的,就是一个函数的调用。下方这个Demo一目了然呢,在此就不做过多赘述了。 2. Assertions(断言) 在单元测试中是少不了断言的,Swift中的断言和Objective-C的区别不是太大,使用方法也是大同小异。下方就是断言的两种方法,由代码提示可知,在断言中的提示条件是可选的。断言会在Debug模式下起作用,但是在Release版本中就会被忽略。 在assert()函数中, 第一个参数是Bool类型,第二个参数是输出的信息。当条件为true时,断言不执行,相应的断言信息不打印。当条件为false时,断言执行,并且打印相应的断言信息。 assertionFailure()函数只有一个Message参数,并且该参数也是可以省略的,assertionFailure()没有条件。如下所示: 3. 先决条件(Preconditions) Preconditions的用法和断言一样,不过有一点需要主要,Preconditions在debug和release模式下都会被执行,除非使用–Ounchecked进行编译。下方截图是代码提示给出的Preconditions函数的提示,如下所示: 关于Preconditions的具体用法请参照断言,和断言用法一样,在此就不做过多的赘述了。 二.Swift中的错误处理 在Objective-C中,如果你处理过错误的话,那么你将会对NSError很熟悉。在Swift中,如果你要定义你自己的错误类型,你只需要实现ErrorType协议即可。声明完错误类型后,就可以在处理错误抛出异常时使用自定义的错误类型了。下方将会一步步带你走完Swift中的错误处理的路程。 1.使用枚举创建错误类型 (1).遵循ErrorType协议,自定义错误类型。下方定义了一个错误类型枚举,该枚举遵循了ErrorType协议,在接下来的代码中我们将会使用这个MyCustomErrorType枚举,错误枚举的实现如下所示: //定义错误类型 enum MyCustomErrorType: ErrorType { case ErrorReason1 case ErrorReason2 case ErrorReason3 } (2).在我们的函数定义时可以使用throws关键字,以及在函数中使用throw关键字对错误进行抛出,抛出的错误类型就可以使用上面我们自己定义的错误类型。下方函数就是一个可以抛出错误的函数,抛出的错误就是我们在上面枚举中所定义的类型。具体代码如下所示: func myThrowFunc1() throws { let test:Int? = nil guard test != nil else { throw MyCustomErrorType.ErrorReason1 } } (3).上面函数的功能是对错误进行抛出,接下来就该使用do-catch来处理抛出的错误。使用try对错误进行捕捉,使用do-catch对错误进行处理。具体处理方式如下所示。在下方错误处理中类似于switch-case语句,catch后边可以枚举匹配错误类型,具体如下所示: (4)在枚举实现错误类型中我们可以通过值绑定的形式为错误添加错误代码和错误原因。在声明枚举时,我们使用了枚举元素值绑定的特性(关于枚举使用的更多细节请参考之前的博客《窥探Swift之别样的枚举类型》)。在声明枚举成员ErrorState时,我们为其绑定了两个变量,一个是错误代码errorCode, 另一个是错误原因errorReason。这两者可以在抛出错误时为其传入相应的值,如下方代码片段中的throwError函数所示,在抛出错误是为errorCode指定的错误代码为404,为errorReason指定的错误原因是“not found”。 最后就是使用do-catch处理异常了,在catch中对绑定的错误代码和错误原因进行了获取,并且通过where子句进行了错误代码的筛选。此处catch的用法与switch-case中获取枚举绑定值的用法是一样的,所以在此就不做过多的赘述。具体实现方式如下代码所示: 2.使用结构体为错误处理添加Reason 在上面的内容中,使用枚举遵循ErrorType协议的方式定义了特定的错误类型。接下来我们将使用结构体来遵循ErrorType协议,为错误类型添加错误原因。也就是说,我们可以在抛出错误时,给自定义错误类型提供错误原因。该功能在开发中是非常常用的,而且用起来也是非常爽的。接下来就看一下如何为我们的错误类型添加错误原因。 (1)使用结构体创建错误类型,下方名为MyErrorType的结构体遵循了ErrorType协议,并且在MyErrorType结构体中,声明了一个reason常量,该reason常量中存储的就是错误原因,具体实现方式如下: 1 struct MyErrorType: ErrorType { 2 let reason : String 3 } (2)上面定义完错误类型结构体后,在错误抛出中就可以使用了。在错误抛出时,可以传入一个错误原因,具体代码如下所示: func myThrowFunc2() throws { let test:Int? = nil guard test != nil else { throw MyErrorType(reason: "我是详细的错误原因,存储在error中") } } (3)最后要对抛出的错误进行do-catch处理,在处理时,可以对错误原因进行打印,错误原因存储在error中,具体操作和打印结果如下所示: 由上面的输出结果可知,error是我们自定义的MyErrorType类型,我们可以使用下面的代码来代替catch中的print语句,如下所示: 上面的做法似乎有些麻烦,还有一种简化输出的方法,就是在上述结构体中实现CustomDebugStringConvertible协议,对描述信息进行一个重写,就可以在打印error时,只打印错误信息,下方是重写后的结构体。 struct MyErrorType: ErrorType,CustomDebugStringConvertible { let reason : String var debugDescription: String { return "错误类型-----\(self.dynamicType): \(reason)" } } 修改后,输出结果如下,直接打印error输出的就是错误信息,而不是MyErrorType类型。 3.使String类型遵循ErrorType协议,直接使用String提供错误原因 在“2”中,我们使用了结构体遵循ErrorType协议的形式,来为错误提供错误信息的。在接下来的部分,我们将通过更为简单的方式为抛出的错误提供错误信息。这种方式更为简单,也易于理解,具体方式如下方代码所示: 三、在错误处理中使用内置关键字 1.初探这些内置关键字 在Swift中提供了一些内置关键字(__FILE__, __FUNCTION__, __LINE__等)来获取上下文信息,在本篇博客的第三部分,将会给出如何在我们的错误处理中使用这些内置关键字。下方就是这些内置关键字的作用,如下所示: 上面说是内置关键字,其实就是存储代码上下文的宏定义,上方代码段简单的给出了这些内置关键字的作用与用法,在接下来将在ErrorType中使用这些内置关键字,让我们的错误信息更加丰富多彩。 2.在ErrorType中使用上述内置关键字 如果想在ErrorType中使用这些上下文内置关键字,我们只需要对ErrorType进行扩展,使其在ErrorType提供错误信息时给出出错的上下文信息。当然,这实现起来比较简单,就是在ErrorType中添加了一个扩展方法contextString()。该方法的作用就是提供错误的上下文信息,也就是在出错的地方,调用contextString()方法生成上下文描述信息即可。对ErrorType协议的具体延展实现如下代码段所示. 在下方代码片段中,我们对ErrorType进行了扩展,为ErrorType添加了contextString的函数实现。contextString()函数有三个默认参数,分别是file--当前文件名,function--当前出错的函数名,line--当前抛出异常的行数。上述三个参数都有参数默认值,分别对应着__FILE__, __FUNCTION__, __LINE__。该扩展函数的返回值为这三个参数组成从字符串信息。具体实现如下所示: 3.使用扩展的contextString方法 上面我们使用结构体实现ErrorType协议的形式,为错误类型添加错误原因。接下来我们将在添加reason的同时,使用contextString()函数添加描述信息。下方CustomErrorType结构体遵循了ErrorType协议,其中添加了一个reason常量来存储错误原因,一个context常量来存储上下文信息,并且为该结构体添加了一个构造函数,在构造函数中初始化和reason常量。具体实现如下所示: 4. 抛出并捕获异常 在下方代码中函数throwError()抛出了异常,该抛出的错误类型是CustomErrorType。在创建CustomErrorType类型实例,也就是err变量时,我们指定了错误原因,也就是为reason赋了一个值。在创建完err实例后,我们又调用延展contextString()函数获取异常的上下文信息,并把返回的内容存储在err实例的context属性中。最后使用throw关键字抛出err实例,如下方第一部分代码所示。 在创建抛出异常的函数后,我们需要对抛出的异常进行捕获。也就是使用try对异常进行捕获,使用do-catch对异常进行处理,具体操作如下方第二段代码所示。 5. 分析打印结果 经过上述步骤如果你在Playground中进行试验的,那么在控制台上你将会看到如下信息。从打印出的信息我们可以看到,信息包括reason:错误原因,和context:异常上下文。在下方的输出结果中,文件名我们可以看到是<EXPR>这并不是确切的文件名,因为我们是在Playground中使用的,并且不是确切的Swift源文件,所以获取不到确切的文件名。 为了观察确切的文件名,我们需要在确切的Swift源文件中抛出上述异常。在特定Swift源文件中,我们会看到下方的输出结果。从下方的输出日志中,我们可以清楚的看到文件名是一个详细的文件路径。如下所示: 今天的博客内容也够多的了,就先到这儿吧,以后在做小的Demo时,如果用到其他的错误处理方式,在做详细介绍呢。
重构是项目做到一定程度后必然要做的事情。代码重构,可以改善既有的代码设计,增强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,我们在项目中所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但这些功能模块有可能在下一次需求中不在适用。或者你因为需求迭代与变更,使你原有的方法或者类变得臃肿,以及各个模块或者层次之间耦合度增加。此时,你要考虑重构了。 重构,在《重构,改善既有代码的设计》这本经典的书中给出了定义,大概就是:在不改变代码对外的表现的情况下,修改代码的内部特征。说白了,就是我们的测试用例不变,然后我们对既有的代码的结构进行修改。重构在软件开发中是经常遇到的,也是非常重要的。在需求迭代,Debug,Code Review时,你都可以对你既有的代码进行重构。 在接下来的几篇博文中,我想与大家一块去窥探一下代码重构的美丽,学习一下代码重构的一些规则。当然在每个规则中都有小的Demo, 在本篇博客以及相关内容的博客是使用Swift语言实现的。当然,和设计模式相同,重构重要的是手法和思想,和使用什么样的语言关系不大。经典的重构书籍中是使用Java语言来实现的,如果你对PHP, Python等其他语言比较熟悉,完全可以使用这些语言来测试一些重构手法。 本篇博客的主题就是通过一些列的重构手法,对既有的需要重构的函数或者方法进行重构。并且会将每个示例在GitHub上进行分享,感兴趣的小伙伴可以对其进行下载。有的小伙伴说了,我没有Mac,怎么对你写的Swift代码进行编译呢?这个问题好解决,你可以看我之前发表的这篇博客《窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型》。你可以将相关代码进行拷贝,在浏览器中观察结果。因为在线编译的网站是国外的,访问起来也许会有一些卡顿,不过是可以用的。好前面扯了这么多了,进入今天的主题。 一、Extract Method(提取函数)-------将大函数按模块拆分成几个小的函数 Extract Method被翻译成中文就是提取函数的意思,这一点在代码重构中用的非常非常的多。在重构时提倡将代码模块进行细分,因为模块越小,可重用度就越大。不要写大函数,如果你的函数过大,那么这意味着你的函数需要重构了。因为函数过大,可维护性,可理解性就会变差。并且当你实现类似功能的时候就容易产生重复代码。写代码时,最忌讳的就是代码重复。这也就是经常所说的DRY(Don`t Repeat Yourself)原则。所以当函数过长时,你需要将其细分,将原函数拆分成几个函数。 下方将会通过一个示例来直观的感受一下Extract Method,当然这些示例不是我原创的,是《重构:改善既有代码的设计》中Java示例演变的Swift版,在写Swift代码时,对原有的示例进行了一些修改,算是伪原创吧。不过目的只有一个:希望与大家交流分享。实在是没有必要再找其他的例子说明这些重构规则,因为《重构:改善既有的代码的设计》这本书真的是太经典了。 1.需要重构的代码如下所示。下方代码中的MyCustomer类中有两个常量属性,并且该类提供了一个构造器。该类还提供了一个输出方法,就是第一该类中的属性进行打印说明,其实该类中没有什么功能。 在写好需要重构的类后,我们要为该类写一个测试用例。这便于在我们重构时对重构的正确性进行验证,因为每次重构后都要去执行该测试用例,以保证我们重构是正确的。下方截图就是为上方示例写的测试用例以及该测试用例的打印结果。当然重构后我们也需要调用该测试用例,并观察打印结果是否与之前的一致。当然如果你不想自己观察,你可以为上面的类添加相应的单元测试,这也是在常规项目中经常使用的。至于如果添加测试用例,我们会在后面的博客中进行详细介绍。下方就是上述类的测试用例和输出结果: 2.接下来我们对上面类中的printOwning函数进行分析。上述类可以正常工作,这是肯定的。但是printOwning()函数写的不够好。因为它干了太多的事情,也就是说该函数包括了其他子模块,需要对其进行拆分。由上面截图中的红框可以看出,每个红框就代表着一个独立的功能模块,就说明这一块代码可以被拆分成独立的函数。在拆分子函数时,我们要为该函数起一个与改代码块功能相符合的名字。也就是说当你看到该函数名字时,你就应该知道该函数是干嘛的。 下方代码段就是我们重构后的类。说白的,就是对函数中可以进行独立的模块进行提取,并为提取的新的函数命一个合适的名称。经过重构后printOwing()函数就只有两行代码,这样看其中调用的函数名也是很容易理解其作用的。下方拆分出来的三个函数也是一个独立的模块,因为函数短小,所以易于理解,同时也易于重用。经过Extract Method,当然好处是多多的。经过重构后的代码,我在调用上述的测试用例,输出结果和原代码是一直的,如果不一致的话,那么说明你的重构有问题呢,需要你进行Debug。 二. Inline Method ---- 内联函数:将微不足道的小函数进行整合 看过《周易》的小伙伴应该都知道,《周易》所表达的思想有一点就是“物极必反”。《周易》中的六十四卦中的每一卦的“上九”(第六位的阳爻)或者“上六”(第六位的阴爻)都是物极必反的表现。其实《周易》其实就是计算机科学中二进制的表象,因为太极生两仪(2进制中的2),两仪生四象(2的平方为4),四象生八卦(4 x 2 = 8),八卦有演变出六十四卦。六十四卦的就是2进制中的0-1排列。九五至尊,九六就物极必反了。wo kao, 扯远了,言归正传,当然这提到《周易》不是说利用周易如何去算卦,如何去预测,本宝宝不信这东西。不过《周易》中的哲学还是很有必要学习一下的。有所取,有所不取。 回到本博客的主题,Inline Method其实是和Extract Method相对的。当你在重构或者平时编程时,对模块进行了过度的封装,也就是使用Extract Method有点过头了,把过于简单的东西进行了封装,比如一个简单的布尔表达式,而且该表达式只被用过一次。此时就是过度的使用Extract Method的表现了。物极必反,所以我们需要使用Inline Method进行中和,将过度封装的函数在放回去,或者将那些没有必要封装的函数放回去。也就是Extract Method相反的做法。 至于Inline Method规则的示例呢,在此就不做过多的赘述了,因为只需要你将Extract Method的示例进行翻转即可。 三.Replace Temp with Query----以查询取代临时变量: 将一些临时变量使用函数替代 1.Replace Temp with Query说白了就是将那些有着复杂表达式赋值并且多次使用的临时变量使用查询函数取代,也就是说该临时变量的值是通过函数的返回值来获取的。这样一来在实现类似功能的函数时,这些复杂的临时变量就可以进行复用,从而减少代码的重复率。下方就是Replace Temp with Query规则的一个特定Demo,接下来我们要对getPrice()函数使用Replace Temp with Query规则进行重构。 对上面的小的demo创建对应的测试用例是少不了的,因为我们要根据测试用例还测试我重构后的代码是否一致,下方截图就是该代码的测试用例以及输出结果,具体如下所示。 2.接下来就是对Procut类中的getPrice()函数进行分析并重构了。在getPrice()函数中的第一个红框中有一个basePrice临时常量,该常量有一个较为复杂的赋值表达式,我们可以对其使用Replace Temp with Query进行重构,可就是创建一个函数来返回该表达式的值。第二个红框中的discountFactor临时变量被多次使用,我们可以对其通过Replace Temp with Query规则进行重构,具体重构后的代码如下所示。 由重构后的代码容易看出,上面我们提到的临时常量或者变量都不存在了,取而代之的是两个查询方法,对应的查询方法返回的就是之前消除的临时变量或常量的值。 四、Inline Temp ---内联临时变量:与上面的Replace Temp with Query相反 当临时变量只被一个简单的表达式赋值,而该临时变量妨碍了其他重构手法。此时我们就不应该使用Replace Temp with Query。之所以有时我们会使用到Inline Temp规则,是因为Replace Temp with Query规则使用过度造成的情况,还是物极必反,使用Replace Temp with Query过度时,就需要使用Inline Temp进行修正,当然Inline Temp的示例与Replace Temp with Query正好相反,在此就不做过多的赘述了。 五、Introduce Explaining Variable---引入解释性变量:将复杂的表达式拆分成多个变量 当一个函数中有一个比较复杂的表达式时,我们可以将表达式根据功能拆分成不同的变量。拆分后的表达式要比之前未拆分的表达式的可读性更高。将表达式拆分成相应的临时变量,也就是Introduce Explaining Variable,如果临时变量被多次使用的话,我们还可以尝试着使用Replace Temp with Query规则去除临时变量,也就是将临时变量换成查询函数。 1.在下方Product类中的getPrice()方法中返回了一个比较长的表达式,第一眼看这个函数感觉会非常的不舒服。因为它返回的表达式太长了,而且可读性不太好。在这种情况下就很有必要将该表达式进行拆分。 2.接下来就可以使用Introduce Explaining Variable规则,引入解释性变量。顾名思义,我们引入的变量是为了解释该表达式中的一部分的功能的,目的在于让该表达式具有更好的可读性。使用Introduce Explaining Variable规则,就相当于为该表达式添加上相应的注释。下方截图就是使用 Introduce Explaining Variable规则进行重构后的结果。 3.引入临时变量是为了更好的可读性,如果临时变量所代表的表达式多次使用,我们就可以对上述函数在此使用Replace Temp with Query规则进行重构。也就是去除经常使用而且表达式比较复杂的临时变量,下方代码段是对上述函数进行Replace Temp with Query重构,去掉临时变量,再次重构后的结果如下所示。 六、Split Temporary Variable-----分解临时变量:一心不可二用 什么叫分解临时变量的,具体说来就是在一个函数中一个临时变量不能做两种事情,也就是一个临时变量不能赋上不同意义的值。如果你这么做了,那么对不起,请对该重复使用的临时变量进行分解,也就是说你需要创建一个新的临时变量来接收第二次分配给第一个临时变量的值,并为第二个临时变量命一个确切的名字。 下方第一个函数是重构前的,可以看出temp被重复的赋值了两次的值,如果这两个值关系不大,而且temp不足以对两个值的意思进行说明。那么就说明该段代码就应该被重构了。当然,重构的做法也是非常简单的,只需要术业有专攻即可,各司其职,并且为每个临时变量命一个合适的名字即可。具体做法如下所示。 七、Remove Assignments to Parameters----移除对参数的赋值 “移除对参数的赋值”是什么意思呢?顾名思义,就是在函数中不要对函数参数进行赋值。也就是说你在函数的作用域中不要对函数的参数进行赋值(当然,输入输出参数除外),当直接对函数的参数进行修改时,对不起,此时你应该对此重构。因为这样会是参数的原始值丢失,我们需要引入临时变量,然后对这个临时变量进行操作。 1.下方这个discount()函数就做的不好,因为在discount()函数中直接对非inout参数inputVal进行了修改并且返回了,我们不建议这样做。遇到这种情况,我们需要使用Remove Assignments to Parameters规则对下方的函数进行重构。 2.当然重构的手法也特别简单,就是需要将上面的inputVal使用函数的临时变量进行替代即可,下方就是重构后的函数。 八.Replace Method with Method Object----以函数对象取代函数 当一个特别长的函数,而且函数中含有比较复杂的临时变量,使用上述那些方法不好进行重构时,我们就要考虑将该函数封装成一个类了。这个对应的类的对象就是函数对象。我们可以将该场函数中的参数以及临时变量转变成类的属性,函数要做的事情作为类的方法。将函数转变成函数类后,我们就可以使用上述的某些方法对新的类中的函数进行重构了。具体做法请看下方示例。 1.下方示例中的discount函数有过多的参数(当然,现实项目工程中参数比这个还要多),并函数中含有多个临时变量,假设函数功能比较复杂,而且比较长。下方示例对该函数使用上述那些规则进行重构会比较复杂,此时我们就可以将该函数抽象成一个类。 2.重构的第一步就是将上述discount()函数抽象成Discount类。在Discount类中有六个属性,这六个属性分别对应着discount()函数的不同参数。除了添加参数属性外,我们在函数类提取时还添加了一个Account的委托代理对象。该委托代理对象是为了在Discount类中访问Account类中依赖的数据,下方是第一次重构后的代码。 3.接着,我们就可以在新的Discount类中的compute()方法中使用我们上述介绍的规则进行重构。对compute()方法进行分析,我们发现importandValue等属性是可以通过Replace Temp with Qurey 规则进行消除的。所为我们可以再次对上述方法进行重构,重构后的具体代码如下: 今天的博客主要讲了如何对既有代码中的函数进行重构,在本篇博客中提到了8大规则。这8大规则在函数代码重构时时非常实用的,并且也是非常重要的。还是那句话,虽然代码是使用Swift语言实现的,但是代码重构的手法和思想和语言无关。接下来还会继续更新关于代码重构的博客,敬请期待吧。
在上篇博客《代码重构(一):函数重构规则(Swift版)》中,详细的介绍了函数的重构规则,其中主要包括:Extract Method, Inline Method, Inline Temp, Replace Temp with Query, Introduce Explaining Variable, Split Temporary Variable, Remove Assignments to Parameters, Replace Method with Method Object等。关于上述这些函数重构的规则更为详细的信息请参考上一篇博客,在此就不做过多的赘述了。 今天这篇博客主要介绍一下类的重构。在我们写代码时,有些类是不规范的,需要重构。在对类进行重构时,也是有一些章法可寻的,本篇博客就结合着相关示例,对类的重构进行相关的介绍。当然在本篇博客中使用的实例,还是延续上一篇文章的风格,仍然采用Swift语言进行编写。当然,还是那句话,重构的思想和手法与设计模式类似,都与具体语言实现无关。触类旁通,关键还是思想和手法。为了精简博文的篇幅,相关的测试用例就不往上粘贴了。当然,在你实现时,测试用例是必不可少的,因为测试用例可以在你重构时及时发现因为重构而产生的错误。言归正传,进入今天博客的主题。 一、Move Method----方法迁移 关于Move Method,首先谈论一下为什么要进行方法的迁移。原因很简单,就是当类中的方法不适合放在当前类中时,就应该为该方法寻找合适下家。那么怎样才可以称作是当前方法不适合在当前类中呢?一个类中的函数与另一个类有很多的交互,函数非常依赖于某个类。如果一个类有太多行为,或者与另一个类有太多合作而形成高度耦合。此时就应该将该方法搬移到其高度依赖的类中。 在给方法搬家时需要做的就是在方法的新家中创建一个方法,实现要搬移的功能,如果新创建的函数需要旧类中的数据,那么就创建一个委托对象来解决这个问题。说白了就是在另一个类中创建一个相同的功能的新函数,将旧函数变成一个单纯的委托函数,或者将旧函数完全移除。搬移后,我们可以再使用函数的重构规则对新组的函数进行重构。下方就通过一个实例来直观的感受一下Move Method。 1.代码实例 在下方截图中有两个类,一个Book类,另一个是BookCustomer类。在Book类中有两个属性,一个是bookCode:表示书的种类(NEW_BOOK,OLD_BOOK, CHIDREN_BOOK), 另一个属性就是书名bookName。在BookCustomer中有3个字段,name表示用户的名称,isVip表示用户是否是会员,books表示该用户所购买的书的集合。BookCustomer类中的charge()方法用来根据books数组来计算图书的总价格,并返回总价格。如果是VIP, 就在总价格的基础上打7折,普通用户打8折。下方截图就是其具体实现。 2.使用Move Method进行重构 首先我们对上述两个类进行分析,观察需要重构的地方。首先第一眼看代码时,较长的charge()函数会让我们看起来些微的不舒服,因为它太长了。再仔细分析,其中的Switch语句中的业务逻辑用的全是Book类的东西,和当前BookCustomer类没用什么关联。但是这个Switch语句是当前charge()函数的核心,也就是BookCustomer严重依赖Book类的地方。以此分析下去,我们就清楚的指定,该Switch语句块放错了地方,它应该放在Book类中。所以我们应该将这块代码进行搬移。 重构方法就是在Book类中创建一个charge()函数,将Switch语句块放入新的charge()函数中。然后在原来的charge()函数使用Switch语句时调用新的charge()方法。下方代码段是使用Move Method重构后的结果。 3.使用函数重构 在使用Move Method重构后,我们看出在BookCustomer类中的charge()函数是可以使用Extract Method和Replace Temp With Qurey进行重构的。关于这两个函数重构的规则的具体细节请参见《代码重构(一):函数重构规则(Swift版)》中的介绍。下方截图是对BookCustomer类中的charge()函数进行重构后的结果,如下所示: 二、Move Field----搬移字段 上一部分是搬移方法,Move Field(搬移字段)与Move Method适用场景类似。当在一个类中的某一个字段,被另一个类的对象频繁使用时,我们就应该考虑将这个字段的位置进行更改了。Move Field与Move Method的思想和做法差不多,再次对其的示例就省略了。举一反三,你可以类比着Move Method来使用Move Field规则。具体实现方式在此就不做过多的赘述了。 三、Extract Class----提炼类 Extract Class和Extract Method类似,Extract Method提取的是方法,而Extract Class提取的是类。一个类如果过于复杂,做了好多的事情,违背了“单一职责”的原则,所以需要将其可以独立的模块进行拆分,当然有可能由一个类拆分出多个类。当然,对类的细化也是为了减少代码的重复性,以及提高代码的复用性,便于代码的维护。下方将会通过一个实例,对类进行提炼。 1.重构前的代码 下方是我们将要进行重构的代码段。在Person类中有三个字段,常量name表示该Employee的名字,officeAreaCode表示Employee所在办公部门的区域代码。然后就是Employee类的构造函数了。Employee类比较简单。 2.使用Extract Class对Employee重构 接下来要做的就是使用Extract Class对Employee进行重构。因为上述Employee类设计的不好,因为Employee类可以再分。显然可以将区域号和电话号提取成一个TelePhoneNubmer类,在Employee中调用TelePhoneNubmer类。这样一来TelePhoneNubmer类就可以重复利用了,而且层次结构更为清晰。下方代码段就是对上述代码进行重构后的结果。具体如下所示: 四、Inline Class----类的内联化 又到了“物极必反”的时候了。Extract Method与Inline Method职责相反,Extract Class当然也就职责相反的原则。那就是接下来要介绍的类的内联化:Inline Class。如果过度使用Extract Class原则的话,会使得某些类过于简单并且调用该简单的类的地方极少。也就是说一个类根本不能称为一个类,所以我们可以通过Inline Class将过度抽象出来的类放到其他类中。 关于Inline Class的示例在此就不做过多的赘述了,因为与Extract Class原则相反,将第三部分中的示例倒着过一遍即为类的内联化的工作方式。 五、Hide Delegate----隐藏“委托关系” 隐藏类之间的“委托关系”这一原则用起来是非常不错的,它可以简化类调用委托者的方式。简单的说就是讲委托调用的链,封装成相应的方法,使其隐藏掉具体的调用细节,从而简化了调用方式。下方会根据具体事例和测试用例来介绍一下Hide Delegate。 1.重构前的案例 在下方代码片段中有两个类,这两个类互为依赖关系。Department中有People,该People对应的就是经理人。还有一个字段就是chargeCode,对应的是部门代码。而People类中有name--名字字段,department--所属部门字段。在People对象中可以委托department对象来获取经理的名字。 获取People对象所在部门经理的名字的测试用例如下所示。在下方测试用例中创建了一个经理和一个员工,并为员工和经理绑定关系。zeluLi.department.manager.name就是委托department对象来调用经理的名字,这样调用未免太长,所以有必要使用Hide Delegate原则对其进行优化。 2.使用Hide Delegate进行重构 使用Hide Delegate进行重构的方式是比较简单的,就是在People中封装一个方法,在方法中返回经理的对象即可,这样就隐藏掉了委托关系。具体实现方式如下截图所示: 添加上上面的函数后的调用方式如下: Remove Middle Man(移除中间人)原则与Hide Delegate相反,就是没有必要将委托人进行隐藏,所以就使用Remove Middle Man原则将上面我们封装的获取委托人的方法进行移除。关于Remove Middle Man的范例就不做过多的赘述了。 六、Introduce Foreign Method----引入外加函数 这一点在开发中用的还是比较多的,有时候你在不想或者不能修改原类的情况下想为该类添加新的方法。在这种情况下就会使用到Introduce Foreign Method。在Swift语言中,使用Introduce Foreign Method原则特别简单,也就是在不改变类的情况下对类进行扩展也是特别简单的。因为Swift语言以及OC中有延展的功能,所以非常对此非常好实现的。下方的代码段就是对MyTest类使用extension为其扩展一个method2方法,具体如下所示。
在《代码重构(一):函数重构规则(Swift版)》和《代码重构(二):类重构规则(Swift版)》中详细的介绍了函数与类的重构规则。本篇博客延续之前博客的风格,分享一下在Swift语言中是如何对数据进行重构的。对数据重构是很有必要的,因为我们的程序主要是对数据进行处理。如果你的业务逻辑非常复杂,那么对数据进行合理的处理是很有必要的。对数据的组织形式以及操作进行重构,提高了代码的可维护性以及可扩展性。 与函数重构与类重构类似,对数据结构的重构也是有一定的规则的。通过这些规则可以使你更好的组织数据,让你的应用程序更为健壮。在本篇博客中将会结合着Swift代码实现的小实例来分析一下数据重构的规则,并讨论一下何时使用那些重构规则进行数据重构。还是那句话“物极必反”呢,如果不恰当的使用重构规则,或者过度的使用重构规则不但起不到重构的作用,有时还会起到反作用。废话少说,进入今天数据重构的主题。 一. Self Encapsulate Field (自封装字段) "自封装字段"理解起来比较简单,一句话概括:虽然字段对外是也隐藏的,但是还是有必要为其添加getter方法,在类的内部使用getter方法来代替self.field,该方式称为自封装字段,自己封装的字段,自己使用。当然该重构规则不是必须执行的,因为如果你直接使用self来访问类的属性如果不妨碍你做扩展或者维护,那么也是可以的,毕竟直接访问变量更为容易阅读。各有各的好处,间接访问字段的好处是使你的程序更为模块化,可以更为灵活的管理数据。比如在获取值时,因为后期需求的变化,该获取的字段需要做一些计算,那么间接访问字段的方式就很容易解决这个问题,而直接访问字段的方式就不是很好解决了。所以间接一下还是好处多多的,不过直接访问不影响你的应用程序的话,也是无伤大雅的。 下方会通过一个实例来看一下间接访问字段的好处。下方的IntRange类中的字段就没有提供间接访问的方法,在代码中通过直接访问的形式来使用的字段。这种做法对当前的程序影响不大,但是如果提出需求了。要在high赋值后,在IntRange对类进行一个较为复杂的修改。那么对于下方代码而言,有两种解决方案,就是在构函数中进行修改,在一个就是在使用self.high的地方进行修正,当然这两种方法都不理想。最理性的方案是在相应字段的getter方法修改。 下方截图就是为InRange类中相应的字段自封装了getter和setter方法,并在使用self.字段的地方使用该自封装的方法代替(构造函数中对字段的初始化除外,因为设置方法一般在对象创建完毕以后在调用,所以不能在创建对象时调用,当然Swift语言也不允许你在构造函数函数中调用设置方法)。下方红框中的是我们添加的自封装方法,绿框中是对自封装方法的使用,白框中是需要注意的一点,构造函数中不能使用该设置函数。 当然,只添加上上述自封装字段后,优点不明显。当然子类CappedRange继承了IntRange函数后,这种优点就被显示了出来。在子类中CappedRange的high需要与新添加的字段cap进行比较,取较大的值作为区间的上限。在这种情况下自封装字段的优点就被凸显了出来。在子类中只需要对getHigh()函数进行重新,在重写的方法中进行相应的计算即可。因为当在子类中调用inclued()方法时,在include()方法中调用的是子类的getHigh()方法。具体请看下方子类截图: 二. Replace data Value with Object(以对象取代数据值) “以对象取代数据值”说白了就是我们常说的实体类,也就是Model类。Model的职责就将一些相关联的数据组织在一起来表示一个实体。Model类比较简单,一般只用于数据的存储,其中有一些相关联的字段,并为这些相关联的字段添加getter/和setter方法。下方是一个Person的数据模型,我们命名为PersonModel,其中有三个表示Person属性的字段name、birthday、sender。然后提供了一个构造器以及各个属性对应的getter和setter方法。具体请看下方代码所示: 三、Change Value to Reference (将值对象改变成引用对象) 在介绍“将值对象改变成引用对象”之前,我们先去了解一下值对象和引用对象的区别。先说一下值对象,比如两个相等的数值,存入了两个值对象中,这两个值对象在内存中分别占有两块不同的区域,所以改变其中一个值不会引起另一个值得变化。而引用对象正好相反,一个内存区域被多个引用指针所引用,这些引用指针即为引用对象,因为多个指针指向同一块内存地址,所以无论你改变哪一个指针中的值,其他引用对象的值也会跟着变化。 基于值对象和引用对象的特点,我们有时候根据程序的上下文和需求需要将一些值类型改变成引用类型。因为有时候需要一些类的一些对象在应用程序中唯一。这和单例模式又有一些区别,单例就是一个类只能生成一个对象,而“将值对象改变成引用对象”面临的就是类可以创建多个对象,但是这多个对象在程序中是唯一的,并且在某一个引用点修改对象中的属性时,其他引用点的对象值也会随之改变。下方就通过一个订单和用户的关系来观察一下这个规则。 1. 值引用的实例 (1) 首先我们需要创建一个消费者也就是Customer类。Customer类比较简单,其实就是一个数据实体类。其中有name和idCard属性并对应着getter/setter方法,具体代码如下所示: (2)、紧接着我们需要创建一个订单类,在订单创建时我们需要为该订单关联一个Customer(当然这为了简化实例,我们省略了Order中的其他字段)。该Order类的代码也是比较简单的在此就不做过的的赘述了。不过有一点需要注意的是为了测试,我们将customer设计成值类型,也就是每个Order中的customer都会占用不同的内存空间,这也就是值类型的特点之一。 (3).创建完Order与Customer类后,紧接着我们要创建测试用例了。并通过测试用例来发现问题,并在重构时对该问题进行解决。在测试用例中我们创建了三个订单,为每个订单关联一个Customer。从测试用例中可以看出,关联的消费者数据为同一个人,但是这一个人在内存中占用了不同的存储空间,如果一个订单中的用户信息进行了更改,那么其他订单中的用户信息是不会更新的。如果创建完用户后,信息不可更改,虽然浪费点存储空间,但是使用值类型是没用问题的。一旦某个订单修改了用户名称,那么就会出现数据不同步的问题。 2.将Order中Customer改为引用类型(重新设计Order类) 因为在Swift语言中类本身就是引用类型,所以在设计Order时,我们值需要将其中的customer字段改成引用外部的Customer类的对象即可。这样一来多个订单可以引用同一个用户了,而且一个订单对用户信息修改后,其他订单的用户信息也会随之改变。要实现这一点需要对Order的构造函数和customer的设置函数进行修改,将在Order内部创建Customer对象的方式改变成将外部Customer对象的引用赋值给Order中的custom对象。说白了,修改后的Order中的customer对象就是外部对象的一个引用。这种方法可以将值对象改变成引用对象 上面这种做法可以将值对象改变成引用对象,但是代价就是改变Order创建的方式。上面代码修改完了,那么我们的测试用例也就作废了,因为Order的创建方式进行了修改,需要外部传入一个Customer对象,下方截图就是我们修改后的测试用例。(如果你是在你的工程中这么去将值对象修改引用对象的,不建议这么做,下面会给出比较好的解决方案)。 3.从根本上进行重构 上面代码的修改不能称为代码的重构,因为其改变的是不仅仅是模块内部的结构,而且修改了模块的调用方式。也就是说里外都被修改了,这与我们重构所提倡的“改变模块内部结构,而不改变对外调用方式”所相悖。所以在代码重构时不要这么做了,因为上面的这种做法的成本会很高,并且出现BUG的几率也会提高。因为每个使用订单的地方都会创建一个Customer的类来支持订单的创建,那么问题来了,如果同一用户在不同地方创建订单怎么办?所以上面的做法还是有问题的,终归是治标不治本。所以我们要从根本上来解决这个问题。因为该问题是因为Customer数据不同步引起的,所以我们还得从Customer来下手。 该部分的重构,在第一部分的基础上做起。我们本次重构的目标就是“不改变对外调用方式,但能保持每个用户是唯一的”。好接下来就开始我们真正的重构工作。在本次重构中,依照重构的规则,我们不会去修改我们的测试用例,这一点很重要。 (1)从根本解决问题,首先我们对Customer进行重构。在Customer中添加了一个静态的私有变量customers, 该静态私有变量是字典类型。其中存储的就是每次创建的消费者信息。在字典中每个消费者的key为消费者独一无二的身份证信息(idCard)。在添加完上述变量后,我们需要为创建一个工厂方法createCustomer() 在工厂方法中,如果当前传入的用户信息未被存入到字典中,我们就对其进行创建存入字典,并返回该用户信息。如果传入的用户已经被创建过,那么就从字典中直接取出用户对象并返回。具体做法如下所示。 (2)、对Customer类修改完毕后,我们需要在Order中通过Customer的工厂方法来获取Customer类的实例,这样就能保证Order中的customer对象也是引用对象了。不过此时的引用对象是从Customer中获取的,而不是外部传过来的。下方是Order类中对工厂方法的调用,这样做的好处就是,我们只对模块的内部进行了修改,而测试用例无需修改。 (3)、对此次重进行测试,我们任然使用第一部分使用的测试用例。也就是说该模块对外的接口是没有变化的,下方就是对重构后的代码的测试结果。由结果可以看出,在不同订单中的用户,只要是信息一致,那么其内存地址是一致的。也就是经过重构,我们将原来的值对象改成了引用对象。 四、Change Reference to Value(将引用对象改为值对象) 将引用对象改为值对象,该重构规则正好与上面相反。在一些情况下使用值对象更为简单,更易管理,但前提是该值对象很小并且不会被改变。在这种情况下你就没有必要使用引用对象了。从上面的示例来看,使用引用对象实现起来还是较为复杂的。还是那句话,如果你的对象非常小,而且在创建后其中的数据不会被改变,如果需要改变就必须在创建一个新的对象来替换原来的对象。在这种情况下使用值对象是完全可以的。在此就不做过多的赘述了。 不过在使用值对象时,你最好为值对象提供一个重载的相等运算符用来比较值对象中的值。也就是说只要是值对象中的每个属性的值都相同,那么这两个值对象就相等。至于如何对“==” 运算符进行重载就不做过多的赘述了,因为该知识点不是本篇博客的重点。 五、Replace Array or Dictionary with Object(以对象取代数组或字典) 这一点呢和本篇博客的第二部分其实是一个。就是当你使用数组或者字典来组织数据,这些数据组合起来代表一定的意义,这是最好将其定义成一个实体类。还是那句话,定义成实体类后,数据更易管理, 便于后期需求的迭代。下方代码段就是讲相应的字典和数组封装成一个实体类,因为确实比较简单,在此就不做过多的赘述了。具体请参加下方代码段。 六、Duplicate Observed Data(复制“被监测数据”) 这一部分是比较重要的部分,也是在做UI开发时经常遇到的部分。用大白话将就是你的业务逻辑与GUI柔和在了一起,因为UI作为数据的入口,所以在写程序时,我们就很容易将数据处理的方式与UI写在一起。这样做是非常不好的,不利于代码的维护,也不利于代码的可读性。随着需求不断的迭代,版本不断的更新,UI与业务逻辑融合的代码会变得非常难于维护。所以我们还是有必要将于UI无关的代码从UI中进行分离,关于如何进行分层宏观的做法请参加之前发布的博客《iOS开发之浅谈MVVM的架构设计与团队协作》。 今天博客中的该部分是分层的微观的东西,也就是具体如何将业务逻辑从GUI中进行剥离。所以在接下来的实例中是和UI实现有关的,会根据一个比较简单的Demo来一步步的将UI中的业务逻辑进行分离。进入该部分的主题。复制“被监测数据”简单的说,就是将UI提供的数据复制一份到我们的业务逻辑层,然后与UI相应的数据进行关联,UI数据变化,被复制的业务逻辑中的数据也会随之变化。这一点也就是所谓的"响应式编程"吧,关于响应式编程,iOS开发中会经常用到ReactiveCocoa这个框架,关于ReactiveCocoa的内容,请参见之前的博客《iOS开发之ReactiveCocoa下的MVVM》。今天的示例中,使用了一个比较简单的方式来同步这些数据,使用了"事件监听机制"。下方就创建一个比较简单的Demo。 1.创建示例 要创建的示例比较简单,在UI方面,只有三个输入框用来接收加数与被加数,以及用来显示两数之和。然后使用两个UILabel来显示+号与=号。我们要实现的功能就是改变其中一个加数与被加数时,自动计算两个数的和并显示。 要实现上述功能的代码也是比较简单的,总共没有几行,下方这个类就是实现该功能的全部代码。代码的核心功能就是“获取加数与被加数的和,然后在加数与被加数的值有一个改变时,就会计算两者之和,并将和赋值给最后一个输入框进行显示”。具体代码如下所示。 class AddViewControllerBack: UIViewController { //三个输入框对应的字段 @IBOutlet var firstNumberTextField: UITextField! @IBOutlet var secondNumberTextField: UITextField! @IBOutlet var resultTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() } //获取第一个输入框的值 func getFirstNumber() -> String { return firstNumberTextField.text! } //获取第二个输入框的值 func getSecondNumber() -> String { return secondNumberTextField.text! } //加数与被加数中的值改变时会调用的方法 @IBAction func textFieldChange(sender: AnyObject) { self.resultTextField.text = calculate(getFirstNumber(), second: getSecondNumber()) } //计算两个数的值 func calculate(first: String, second: String) -> String { return String(stringToInt(first) + stringToInt(second)) } //将字符串安全的转变成整数的函数 func stringToInt(str: String) -> Int { guard let result = Int(str) else { return 0 } return result } } 2.对上述代码进行分析并重构 因为代码比较简单,所以很容易进行分析。在上述UI代码中,我们很清楚的看到后两个函数,也就是calculate()与stringToInt()函数是数据处理的部分,只依赖于数据,与UI关系不是很大,所以我们可以使用复制“被监测数据”规则将该段业务逻辑代码进行提取重构。重构后UI以及UI对外的工作方式不变。 下方的Calculate类就是我们提取的数据业务类,负责处理数据。在该类中我们创建了三个属性来与UI中的输入框进行对应,这也就是所说的复制“被监测的数据”。因为和也就是resultNumber是由firstNumber和SecondNumber计算而来的,所以我们就把resultNumber定义成了计算属性,而firstNumber和secondNumber为存储属性。并为存储属性提供setter方法。在Calculate类的构造函数中,我们为两个值指定了初始化数据也就是“0”。最下方的那两个函数就是我们从UI中直接拷贝过来的数据,一点没有修改,也是可以工作的,因为这部分代码只依赖于数据,而不依赖于UI。 创建为相应的业务逻辑处理类并提取完业务逻辑后,我们需要将业务逻辑中的数据,也就是复制过来的数据与UI中的数据提供者进行绑定,并返回计算结果。下方红框中就是我们要修改的部分,在UI中我们删除掉处理业务数据的代码,然后创建也给Calculate对象,并在相应的事件监听的方法中更新Calculate对象中的数据。如下所示 七、Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 要介绍本部分呢,我想引用本篇博文中第(三)部分是实例。因为在第三部分的实例中Customer与Order的关系是单向关联的,也就是说Order引用了Customer, 而Customer没有引用Order。换句话说,我们知道这个订单是谁的,但你不知道只通过用户你是无法知道他有多少订单的。为了只通过用户我们就能知道该用户有多少订单,那么我们需要使用到“将单向关联改为双向关联”这条规则。 1. 在Customer类中添加上指向Order类的链 因为Customer没有指向Order类的链,所以我们不能获取到该用户有多少订单,现在我们就要添加上这条链。将单向关联改为双向关联,具体做法是在Customer中添加一个数组,该数组中存储的就是该用户所拥有的订单。这个数组就是我们添加的链。数组如下: 1 //添加与Order关联的链,一个用户有多个订单 2 private var orders:Array<Order> = [] 在Customer中值只添加数组也是不行的呢,根据之前提到的重构规则,我们要为数组封装相应的操作方法的,下方就是我们要在Customer中添加的操作数组的方法。具体代码如下所示://====================添加================== func addOrder(order: Order) { self.orders.append(order) } func getOrders() -> Array<Order> { return self.orders } 在Order类关联Customer时,建立Customer到Order的关联。也就是将当前订单添加进该用户对应的订单数组中,具体做法如下: 与之对应的规则是Change Bidirectional Association to Unidirectional(将双向关联改为单向关联),就是根据特定需求删去一个链。就是说,原来需要双向链,可如今由于需求变更单向关联即可,那么你就应该将双向关联改为单向关联。 八、Replace Magic Number with Synbolic Constant(以字面常量取代魔法数) 这一点说白了就是不要在你的应用程序中直接出现字数值。这一点很好理解,在使用字面数值时,我们要使用定义好的常量来定义。因为这样更易于维护,如果同一个字面数值写的到处都是,维护起来及其困难。当使用字面常量时维护起来就容易许多。该规则比较容易理解,在此不做过多的赘述。看下方实例即可。对于下方的实例而言,如果在版本迭代中所需的PI的精度有所改变,那么对于替换后的程序而言,我们只需修改这个常量的值即可。 func test(height: Double) -> Double { return 3.141592654 * height } //替换 let PI = 3.141592654 func test1(height: Double) -> Double { return PI * height } 九、Encapsulate Field(封装字段) 当你的类中有对外开放字段时,最好将其进行封装,不要直接使用对象来访问该字段,该优缺点与上述的“自封装字段”的优缺点类似。因为直接访问类的字段,会降低程序的模块化,不利于程序的扩充和功能的添加。再者封装是面向对象的特征之一,所以我们需要将字段变成私有的,然后对外提供相应的setter和getter方法。具体做法如下所示。 //重构前 class Person { var name: String = "" init(name: String) { self.name = name } } //重构后 class Person { private var name: String = "" init(name: String) { self.name = name } func getName() -> String { return name } func setName(name: String) { self.name = "China:" + name } } 十、Encapsulate Collection(封装集合) “封装集合”这一重构规则应该来说并不难理解。当你的类中有集合时,为了对该集合进行封装,你需要为集合创建相应的操作方法,例如增删改查等等。下方就通过一个不封装集合的实例,看一下缺点。然后将其重构。关于“封装集合”具体的细节参见下方实例。 1.未封装集合的实例 下方我们先创建一个图书馆图书类,为了简化示例,该图书类只有一个书名。下方代码段就是这个图书类,如下所示: class LibraryBook { private var name: String init(name: String) { self.name = name } func getName() -> String { return self.name } } 紧接着要创建一个借书者,借书者中有两个字段,一个是借书者的名字,另一个是所借书籍的数组。在Lender中我们没有为lendBooks数组封装相应的方法,只为其提供了getter/setter方法,具体代码如下所示。 class Lender { private var name: String private var lendBooks: Array<LibraryBook> = [] init(name: String) { self.name = name } func getName() -> String { return self.name } func setLendBooks(books: Array<LibraryBook>) { self.lendBooks = books } func getLendBooks() -> Array<LibraryBook> { return self.lendBooks } } 紧接着我们要创建一个测试用例,观察这两个类的使用方式。由下面程序的注释可知,首先我们需要创建一个books的数组,该数组就像一个篮子似的,它可以存储我们要借的书籍。让后将创建的书籍添加到该数组中,最后将books赋值给借书人中的lendBooks。如果要对书籍进行修改,那么只有先获取借书人的lendBooks, 然后进行修改,最后再将修改后的值赋值回去。 //先创建一个书籍数组 var books: Array<LibraryBook> = [] //添加要借的书籍 books.append(LibraryBook(name: "《雪碧加盐》")) books.append(LibraryBook(name: "《格林童话》")) books.append(LibraryBook(name: "《智慧意林》")) //创建借书人 let lender: Lender = Lender(name: "ZeluLi") lender.setLendBooks(books) //获取所借书籍 var myBooks = lender.getLendBooks() //对书籍数组修改后再赋值回去 myBooks.removeFirst() lender.setLendBooks(myBooks) 2.为上面的Lender类添加相应的集合操作的方法 由上面的测试用例可以看出,Lender类封装的不好。因为其使用方式以及调用流程太麻烦,所以我们得重新对其进行封装。所以就会用到“Encapsulate Collection”原则。下面我们就会为Lender添加上相应的集合操作的方法。说白了,就是讲上面测试用例做的一部分工作放到Lender类中。下方是为Lender添加的对lendBooks相应的操作方法。下方代码中的Lender类与上面的Lender类中的lendBooks不同,我们使用了另一个集合类型,也就是字典,而字典的key就是书名,字典的值就是书的对象。具体代码如下所示: 经过上面这样一封装的话,使用起来就更为合理与顺手了。用大白话讲,就是好用。下方是我们重新封装后的测试用例,简单了不少,而且组织也更为合理。具体请看下方代码段: 十一、Replace Subclass with Fields(以字段取代子类) 什么叫“以字段取代子类”呢?就是当你的各个子类中唯一的差别只在“返回常量数据”的函数上。当遇到这种情况时,你就可以将这个返回的数据放到父类中,并在父类中创建相应的工厂方法,然后将子类删除即可。直接这样说也许有些抽象,接下来,我们会通过一个小的Demo来看一下这个规则具体如何应用。1.创建多个子类,并每个子类只有一个函数的返回值不同 接下来我们就要创建重构前的代码了。首先我们创建一个PersonType协议(也就是一个抽象类),该协议有两个方法,一个是isMale(),如果是子类是男性就返回true,如果子类是女性就返回false。还有一个是getCode()函数,如果子类是男性就返回“M”,如果是子类是女性就返回“F”。 这两个子类的差别就在于各个函数返回的值不同。下方是PersonType的具体代码。 1 protocol PersonType { 2 func isMale() -> Bool 3 func getCode() -> String 4 } 然后我们基于PersonType创建两个子类,一个是Male表示男性,一个是Female表示女性。具体代码如下:class Male: PersonType { func isMale() -> Bool { return true } func getCode() -> String { return SenderCode.Male.rawValue } } class Female: PersonType { func isMale() -> Bool { return false } func getCode() -> String { return SenderCode.Female.rawValue } } 上述代码的SenderCode是我们自定义的枚举类型,用来表示"M"与“F”,枚举的代码如下:1 enum SenderCode: String { 2 case Male = "M" 3 case Female = "F" 4 } 2.以字段取代子类 从上面的代码容易看出,Male与Female类实现相同的接口,但接口函数在两个类中的返回值是不同的。这时候我们就可以使用“以字段取代子类”的方式来进行重构,下方截图就是重构后的代码片段。 下方代码中,将PersonType声明了一个类,在类中添加了两个字段,一个是isMale,另一个是code,这两个字段恰好是上述两个子类函数中返回的不同值。这也就是使用字段来取代子类,因为有了这两个字段,我们就可以不用去创建子类了,而是直接在PersonType中通过工厂方法根据不同的性别分别给这两个新加的字段赋上不同的值。具体做法如下。 经过上面这段代码重构后,我们就可以调用PersonType的不同的工厂方法来创建不同的性别了。测试用例如下所示: OK~今天博客的内容也够多的了,那就先到这儿。关于重构的其他规则,还会在后期的博客中继续更新。
继续更新有关重构的博客,前三篇是关于类、函数和数据的重构的博客,内容还算比较充实吧。今天继续更新,本篇博客的主题是关于条件表达式的重构规则。有时候在实现比较复杂的业务逻辑时,各种条件各种嵌套。如果处理不好的话,代码看上去会非常的糟糕,而且业务逻辑看上去会非常混乱。今天就通过一些重构规则来对条件表达式进行重构,让业务逻辑更为清晰,代码更以维护和扩展。 今天博客中的代码示例依然是Swift班,在对条件表达式重构时也会提现出Swift的优雅之处,会用上Swift特有的语法及其特点,比如使用guard来取代if-let语句等。如果你的需求的业务逻辑及其复杂,那么妥善处理条件表达式尤为重要。因为对其妥善处理可以提高代码的可读性,以及提高代码的可维护性。说这么多还是来些示例来的直观,下方会根据一些Demo来着重分享一些条件表达式的部分重构规则,当然今天博客中没有涵盖所有的条件表达式的重构规则,更详细的部分请参见经典的重构书籍。 今天所分享的代码段也将会在github上进行分享,分享地址在本篇博文的后方。废话少说了,进入今天的主题。 一.Decompose Conditional(分解条件表达式) 顾名思义,分解条件表达式说白了,就是当你的条件表达式比较复杂时,你就可以对其进行拆分。一般拆分的规则为:经if后的复杂条件表达式进行提取,将其封装成函数。如果if与else语句块中的内容比较复杂,那么就将其提取,也封装成独立的函数,然后在相应的地方进行替换。 下方代码段就是我们将要重构的代码段。因为本篇博客的主题是对条件表达式的重构,所以我们要对象下方的if-else的代码块进行重构。至于下方代码片段中其他不规范以及需要重构的地方我们暂且忽略。因为我们本篇博客的主题是条件表达式的重构。接下来我们就要对下方代码片段中的条件表达式进行分析了。因为下方这段代码毕竟是一个Demo,在这儿我们可以做个假设,假设if后边的表达式比较复杂,然后在if语句块和else语句块中都有一些复杂的处理,代码看上去的大体样子如下所示。 基于对上述代码的结构的假设,接下来我们将要对其进行重构。说白了,就是让将条件表达式中的比较复杂的模块进行拆分与提取。下方代码段就是我们重构后的结构,就是将我们假设比较复杂的模块进行封装,然后在条件表达式中使用函数进行替换。这样的话,在看条件表达式就比较清晰。当然,我们这个Demo的条件表达式不够复杂,并且if和else的逻辑块所做的东西不多。不过我们可以假设一下,如果在比较复杂的情况下,这种重构手法是比较实用的。具体的大家就看重构前与重构后的区别吧。 二、Consolidate Conditional Expression(合并条件表达式) “合并条件表达式”这条规则也是比较好理解的,因为有时候会存在这样的情况,也就是一些条件表达式后的语句体执行的代码块相同。说白了也就是不同的条件有着同样的返回结果。当然一般在你程序设计之初不会出现此问题,因为在我们设计程序时,如果不同的条件返回相同的结果,我们肯定会将其合并的。不过当你在多个版本迭代,多个需求要增加,或者在别人的代码上进行需求迭代的时候,该情况是很有可能发生的。 说这么多,也许有些抽象,那么就直接看下方需要重构的Demo了。当然,下方的Demo中,我们为了测试,其中的条件比较简单。我们假设每个条件表达式是在不同的需求迭代中或者修改bug时添加的,从而造成了下方这种情况(当然下方的情况有些夸张,这也是为了突出要合并条件的情况)。 在上述夸张的Demo中一眼就能看出来如何进行重构了(在日常开发迭代中,因为业务逻辑的复杂性或者多次迭代的原因,往往不是那么一目了然)。接下来我们就要对不同条件,但返回相同结果的部分进行合并。下方就是我们合并后的结果,重构手法就是讲不同的条件表达式使用&&或者||等布尔运算进行合并。 合并后,如果条件比较复杂,那么我们就可以使用本片博客中的第一部分使用的重构规则进行再次重构。下方代码段是进行第二次重构,就是对比较复杂的表达式进行函数封装,具体如下所示。还是那句话,Demo有些夸张,不过用来演示该重构规则也是不错的,思想就这个思想,具体在日常开发中的使用场景还是需要进行琢磨和推敲的。 三、Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 第二部分合并的是条件表达式,本部分是合并的是重复的条件片段。什么叫合并重复的条件片段呢?这种情况也是一般不会在设计程序之初所出现,但是随着时间的推移,项目不断迭代更新,或者需求变更和迭代更新等等,在项目后期维护时比较容易出现重复的条件片段。在开发中是比较忌讳重复的代码的,如果出现重复的代码,那么说明你的代码应该被重构了。 下方代码片段中if与else中有着相同的语句,就是这个print语句。当然这个示例也是比较夸张的,但是足以说明问题。如果你在开发业务逻辑比较复杂的条件表达式时,要谨慎的检查一下有没有下方这种情况。也就是出现了重复的条件片段。这种情况在需求迭代或者变更中是及其容易出现的。当然下方只是我们这儿列举的一个夸张的示例。 对于这个示例而言,我们不难看出,去代码的重复化。将print语句移到条件之外。但是要学会举一反三呢,重要的是重构手法和思想。在真正的项目中,如果你要提取重复的代码段一般还要结合着其他重构手法,比如将重复的部分先提取成一个独立的模块(独立的类或者方法),然后在条件中使用,最后再去重复话。这样一来,重构的思路就比较清晰了。虽然今天的示例比较简单,但是足以表达这个思路。下方是重构后的代码。如果你对下方代码看着不爽的话,完全可以根据之前我们介绍的重构手法“使用查询来替代临时变量”,将下方的代码继续重构,在本章博客中就不做过多介绍了。 四、Remove Control Flag(移除控制标记) “移除控制标记”这一点还是比较重要的,我平时在代码开发中有时候也会使用到标记变量,来标记一些事物的状态。使用标记变量最直观的感受就是不易维护,不易理解。因为在需求变更或者迭代中,你还得维护这标记变量。如果维护一个标记变量简单的话,那么维护多个标记变量就没这么容易了。而且在你的程序中使用标记变量时,不易理解,并且会显得逻辑混乱。当然这是我的直观感受,在写程序时,我尽量会避免使用标记变量。 当然,下方又是一个有点夸张的例子,但是该例子可以说明问题。下方代码中我们使用了一个flag标记变量,当然下方代码没有什么意义了。在平时开发中我们会使用一些标记变量来标记一个或者一些数据的状态,或者一些控件的状态,再次为了简化示例,我们就简单的引入了一个flag标记变量。下方代码不难理解,当i为20时,我们就翻转标记变量的状态,然后if中的语句块就不被执行了。 虽然下方代码片段是我写的,但是我个人看着超级的不舒服。引入的这个flag增加了代码的逻辑复杂度,让代码变得不那么直观。我个人建议,在平时开发中尽量的要少使用标记变量。不到万不得已,不要在你的代码中引入标记变量。如果有,尝试着去除标记变量。 标记变量一般是可以使用其他语句进行替换的,可以使用break、return、continue等等,这个要根据具体情况而定。总之,代码中有标记变量不是什么好的事情。下方代码段就是对上述代码去除标记变量的重构。重构后的代码如下所示,当然还有好多其他去除的方法,此处仅仅给出了一种。 五、Replace Nested Condition with Guard Clauses(以卫语句取代嵌套的条件) 条件表达式的嵌套是令人讨厌的东西。代码中有多层if-else嵌套会降低代码的可读性以及可维护性,如果此时在加上for循环等等其他逻辑语句,想想都可怕。这种业务逻辑较强的代码要慎重对待。尽量不要将if-else进行嵌套,因为嵌套的if-else确实不好理解,如果在出现bug时,更是不好定位bug。要记住,你写的代码不是给机器看的,而是给人看的,这一点非常重要。不光是代码编写规范,也尽量不要使用理解起来比较费劲的语句来实现你的逻辑。 下方我们将创建一种场景,人为的创建多个if嵌套的情况。下方的demo理解起来应该不难,第一个数组中存储的是第二个字典的key,第二个字典中存储的value是下一个字典也就是第三个字典的key,以此类推。将我们在使用从相应的字典中取出的value做为key再次取值时,我们要保证该值不为nil,所以我们要进行if-let判断。if-let所表示的意思是在取值时,如果当前取出的值不为nil,那么就执行if后的语句体,如果为nil,那么就不执行。这样一来,就会出现多层if-let嵌套的情况。 当然,在一些业务逻辑比较复杂的需求中,嵌套的每层if后都跟着不同的表达式,而不仅仅是if-let。因为为了创建这个if嵌套的场景,再次我们使用了if-let嵌套。这么多的if-let嵌套显然不是什么好的事情,所以我们要对此重构。 如果多层if嵌套,会出现一种叫做“厄运金字塔”的现象,因为在if左边会出现一个三角号的空间。这可不是什么好的标志,这样的代码结构一般理解起来会比较困难,维护起来也不是那么的理想。所以下方我们要对上述代码进行结构。要去除上面的嵌套模式,我们可以将if后的条件进行翻转,根据具体需求再引入return、break、continue等卫语句。下方是讲条件进行翻转然后引入了continue语句,代码如下: 该部分的第二段代码要比第一段代码容易理解的多。经过条件翻转+continue,将上述嵌套的条件语句进行了拆分。拆分成了三个独立的if语句,虽然代码结构不同,但是其实现功能都是一样的。不过上面的解决方案在Swift中并不完美。因为Swift语言是非常优雅的,Swift语言在设计的时候就考虑到了这种情况,所以在Swift 2.0时推出了guard语句。在这种情况下使用guard语句再合适不过了,下方代码段就是使用guard语句进行了重构。 使用guard let声明的变量与guard本身同在一个作用域,也就是说下方代码在guard let中声明的变量可以在for循环中直接使用。guard语句的用法就是如果guard 后方的赋值语句所取出的值为nil,那么就会执行else中的语句,否则就会继续往下执行。在else中一般是break、return、continue等卫语句。这种语法形式很好的对上述糟糕的形式进行了解决,而且还易于理解。 六、Replace Condition with Polymorphism(以多态取代条件表达式) 在介绍“以多态取代条件表达式”之前呢,首先要理解面向对象中多态是什么,也就是说多态是干嘛的。顾明思议,多态就是类的不同类型的对象有着不同的行为状态。如果在你的条件表达式中条件是对象的类型,也就是根据对象的不同类型然后做不同的事情。在这种情况下使用多态在合适不过了。如果该部分在设计模式中,应该对应着状态模式这一部分。这就是以多态来取代条件表达式。 下方是一个比较简单的示例,这也正是我们要进行重构的示例。在Book类中有三中类型,也就是我们的书有三种,具体每种书是什么这不是该示例的重点。在Book类实例化时,需要为书的对象指定该书的类型(三种类型中的一种)。在Book类中,还有一个核心方法,那就是计算书的价格。在charge()函数中,根据不同的书的种类,给出了不同的价格。当然在Switch中的分支的计算方法在本例中非常简单,但是我们要假设每个分支的计算非常复杂,而且有着多行代码。 在这种假设的情况下,下方的条件语句是非常糟糕的,因为庞大的业务逻辑增加了代码维护的成本。在这种情况下我们就可以使用多态来取代复杂的条件表达式。 如果想使用多态,引入其他类是必不可少的,而且每个类中也必须有相应的对应关系。“以多态取代条件表达式”的做法的本质是将不同状态的业务逻辑的处理的代码移到相应的类中。在本示例中,我们要创建三种书籍的价格类,并且将上述case中的“复杂”计算移入到相应的书籍类中。因为每个书籍价格中都会有相应的计算方法,也就是charge()方法,所以我们为这三个书籍价格定义了一个协议(接口或者抽象类),在协议中就给出了charge()函数。然后我们就可以将不同种类的书籍实现该协议,在相应的方法中给出价格计算的代码。具体做法如下所示: 引入上述几个类后,在我们的Book中就可以使用多态了。在Book类中添加了一个price字段,这个字段的类型就是我们的Price协议。也就是只要是符合我们的Price协议的对象都可以。然后在Book中也添加了一个charge()方法,在Book中的charge方法做的一件事情就是调用price对象的charge方法。关键的是根据不同的书籍类型创建不同的书籍价格对象。这样一来,我们就把每个分支中的业务逻辑进行了分离,并使用了多态来获取价格。重构后的优点不言而喻。 今天关于“条件表达式的重构”的规则,当然这不是全部的,只是列举了一些常见的,而且经常使用重构规则。篇幅有限,今天的博客就先到这儿,还会继续更新其他的重构规则。
陆陆续续的发表了多篇关于重构的文章了,还是那句话,重构是一个项目迭代开发中必不可少的一个阶段。其实重构伴随着你的项目的整个阶段。在前几篇关于重构的文章中我们谈到了函数的重构、类的重构、数据的重构以及条件表达式的重构,那么今天咱们就来聊聊继承关系的重构。当然还是延续前几篇博客的风格,我们在博客中的代码实例依然使用Swift语言来实现,当然还是那句话,使用什么语言无所谓,关键是看重构的场景以及重构的思想。 “重构”不仅仅可以改善你既有的代码设计,还可以改变你组织代码的思路,使你的程序在设计之初就趋于合理化,利于程序的扩充。重构往往伴随着设计模式的使用,在重构系列的博客结束后,我想系统的给大家分享一下关于设计模式的东西。当然是结合着各种实例。所谓一名Coder,重构和设计模式是必须涉猎的部分,因为这两者可以让你写出更漂亮的代码,当然要想真正的掌握设计模式以及各种重构手法,还得结合不同的实例来进行实践。理论固然重要,但是要想将理论的东西变成你自己的,还必须将理论付诸实践。废话少说,进入今天的主题。 一.Pull Up Field (字段上移) & Pull Down Field (字段下移) 字段上移与字段下移是相对的,也是我们之前所说的“凡事都有其两面性”,我们要辩证的去看待。我们只对Pull Up Field (字段上移) 这个规则做讨论,那么关于Pull Down Field (字段下移)我们不做过多的讨论,因为这两条规则是相反的,理解一条后,把这条规则反过来就是我们要理解的另一条规则。这样说起来,还是比“举一反三”要容易的多。 下方这个实例是为了解释“字段上移”所实现的一个Demo。当然Demo看上去不仅简单而且是有些夸张的,不过说明字段上移这个规则是完全足够了的。比如我们有一个父类为MySuperClass,我们有一个子类SubClass1,而在SubClass1中有一个字段父类是没有的。因为后期需求迭代或者需求变更,我们需要再创建一个SubClass1的兄弟类,就是下方的SubClass2。在SubClass2中与SubClass1中存在相同的字段,那就是var a = 0。 在上述情况下,就需要使用到我们的“字段上移”的规则。也就是说将子类中相同的字段移到父类中。在该实例中就是讲var a = 0 移到父类中。重构后的代码如下所示: 而将“Pull Down Field (字段下移)”正好与上面的情况相反。也就是父类中有某些字段,但是这些字段只有在少数子类中使用到,在这种情况下我们需要将这个字段移到相应的子类中即可。除了Pull Up Field (字段上移) & Pull Down Field (字段下移) 这两个规则外,Pull Up Method (将函数上移) 和 Pull Down Method (将函数下移)这两个规则与上述情况类似。就是将上面的字段改成函数,有时候不仅字段会出现上述情况,函数也会出现上述情况,需要我们进行移动。因为使用场景类似,再次就不做过多的赘述了。 二、Extract Subclass (提炼子类) 这种情况下用的还是比较多的,当类中的某些方法只有在特定的类的实例中才会使用到,此时我们就需要提炼出一个子类,将该方法放到相应的子类中。这样一来我们的每个类的职责更为单一,这也就是我们常说的“单一职责”。 在下方示例中,CustomerBook是一个图书消费者的类。其中customeCharge()方法是普通用户计算消费金额所需的方法,而vipCharge()方法是VIP用户调用的方法,在内部vipCharge()需要调用customeCharege()方法。但是对外部而言,vipCharge()方法只有VIP用户才会用到,在这种情况下我们就需要使用“Extract Subclass (提炼子类)”规则对VIP进行提炼。 具体做法是我们需要提炼出一个子类,也就是说将VIP用户作为普通用户的子类,然后将只有VIP用户才调用的方法放到我们的VIP子类中。这样一来层次更加明确,每个类的职责更为单一。上述示例重构后的结果如下所示。 与“提炼子类”规则相对应的是“Collapse Hierarchy (折叠继承关系)”。一句话来概括:就是当你的父类与子类差别不大时,我们就可以将子类与父类进行合并。将上面的示例翻转就是“Collapse Hierarchy (折叠继承关系)”规则的示例,再次就不做过多的赘述了。 三、Form Template Method (构造模板函数) Form Template Method (构造模板函数)这一规则还是比较实用的。先说模板,“模板”其实就是框架,没有具体的实现细节,只有固定不变的步骤,可以说模板不关心具体的细节。举个栗子,像前段时间比较火的“秘密花园”,那些没有颜色的线条就是模板,如果一些人获取的是同一本秘密花园,那么说明每个人所获取的模板是相同的。但是每个人对每块的区域所图的颜色又有差异,这就是实现细节的不同。 言归正传,当两个兄弟类中的两个函数中的实现步骤大致一直,但是具体细节不同。在这种情况下,我们就可以将大体的步骤提取成模板,放到父类中,而具体细节由各自的子类来实现。具体实现请看下方的类,在Subclass1和Subclass2中的calculate()方法中的大体步骤是相同的,就是对两个值相加,然后返回这两个值的和。但是具体细节不同,可以看出两个相加值的具体计算方式不同。 在上述情况下我们就可以使用“Form Template Method (构造模板函数)”规则将相同的计算流程进行提取,也就是构造我们的模板函数。将模板函数放到两个类的父类中,然后在相应的子类中只给出实现细节即可。下方代码段是重构后的代码,父类中多出的方法就是我们提取的模板函数,而子类中只给出相应的实现细节即可。 四、以委托取代继承(Replace Inheritance with Delegation) 有时候我们为一些类创建子类后,发现子类只使用了父类的部分方法,而且没有继承或者部分继承了父类的数据。在这种情况下我们就可以将这种继承关系修改成委托的关系。具体做法就是修改这种继承关系,在原有子类中添加父类的对象字段,在子类中创建相应的方法,在方法中使用委托对象来调用原始父类中相应的方法。 下方示例是我们假想出来的,但是说明该规则是绰绰有余了。我们假设SubClass01类中只会用到SuperClass01中的display()方法,而没有继承父类中的数据。在下方示例中是继承关系,在这种情况下我们需要将其转换成委托关系。 下方是我们重构后的代码,在下方代码中我们去除了之前的继承关系。并在子类中创建了一个之前父类的代理对象,并且创建了一个相应的方法,在该新建的方法中通过代理对象来调用相应的方法。具体如下所示。 上述规则与以继承取代委托(Replace Delegation with Inheritance)原则相对于,使用情况与上述相反,再次就不做过多的赘述了。 几天博客就先到这儿,内容比较简单,但是还是比较重要的。
无论做什么事情呢,都要善始善终呢。前边连续发表了5篇关于重构的博客,其中分门别类的介绍了一些重构手法。今天的这篇博客就使用一个完整的示例来总结一下之前的重构规则,也算给之前的关于重构的博客画一个句号。今天的示例借鉴于《重构,改善既有代码的设计》这本书中的第一章的示例,在其基础上做了一些修改。今天博客从头到尾就是一个完整的重构过程。首先会给出需要重构的代码,然后对其进行分析,然后对症下药,使用之前我们分享的重构规则对其进行一步步的重构。 先来聊一下该示例的使用场景(如果你有重构这本书的话,可以参加第一章中的示例,不过本博客中的示例与其有些出入)。就是一个客户去DVD出租的商店里进行消费,下方的程序是给店主用的,来根据用户所借的不同的DVD种类和数量来计算该用户消费的金额和积分。需求很简单而且也不难理解。今天博客会给出原始的代码,也是需要进行重构的代码。当然原始代码完全符合需求,并且可以正确执行。废话少说,先看示例吧。 一、需要重构的代码 在本篇博客的第一部分,我们先给出完成上述需求需要重构的代码。然后在此基础上进行分析,使用之前我们提到过的重构手法进行重构。首先我们给出了电影类的实现。在Movie类中有电影的种类(静态常量):普通电影、儿童电影、新电影,然后有两个成员变量/常量是priceCode(价格代码)、title(电影名称),最后就是我们的构造方法了。该Movie类比较简单,在此就不做过多的赘述了。 实现完Movie类接下来就是租赁类Rental,这个Rental类的职责就是负责统计某个电影租赁的时间。下方就是这个租赁类,该类也是比较简单的,其中有两个字段,一个是租了的电影,另一个就是租赁的时间了。 接下来要实现我们的消费者类了,也就是Customer类。在Customer类中有消费者的名字name和一个数组,该数组中寸的就是租赁电影的集合。其中的statement()方法就是结算该客户的结算信息的方法,并将结果进行打印。在此我们需要了解的需求是每种电影的计价方式以及积分的计算规则。 电影价格计算规则: 普通片儿--2天之内含2天,每部收费2元,超过2天的部分每天收费1.5元 新片儿--每天每部3元 儿童片--3天之内含3天,每部收费1.5元,超过3天的部分每天收费1.5元 积分计算规则: 每借一步电影积分加1,新片每部加2 statement()函数中所做的事情就是根据上面的计算规则,根据用户所租赁的电影的不同来进行金额的计算和积分的计算的。 如果你看代码不太直观的话,下面我使用了startUML简单的画了一个UML的类图来说明上述三个类中的依赖关系。具体如下所示: 在对上面代码重构之前呢,我们还必须有上述代码的测试用例。因为在每次重构之前,我们修改的是代码的内部结构,而代码模块对外的调用方式不会变的。所以我们所创建的测试用例可以帮助验证我们重构后的程序是否可以正常的工作,是否重构后还符合我们的需求。下方就是我们创建的测试用例(当然,在iOS开发中你可以使用其他的测试框架来进行单元测试,重构时,单元测试是少不了的)。在本篇博客中重构后的代码仍然使用下方的测试用例。 //测试用例-------------------------------------------------------------------- //创建用户 let customer = Customer(name: "ZeluLi") //创建电影 let regularMovie:Movie = Movie(title: "《老炮儿》", priceCode: Movie.REGULAR) let newMovie:Movie = Movie(title: "《福尔摩斯》", priceCode: Movie.NEW_RELEASE) let childrenMovie:Movie = Movie(title: "《葫芦娃》", priceCode: Movie.CHILDRENS) //创建租赁数据 let rental1:Rental = Rental(movie: regularMovie, daysRented: 5) let rental2:Rental = Rental(movie: newMovie, daysRented: 8) let rental3:Rental = Rental(movie: childrenMovie, daysRented: 2) customer.rentals.append(rental1) customer.rentals.append(rental2) customer.rentals.append(rental3) let result = customer.statement() print(result) 针对上述案例,上面测试用例的输出结果如下。在每次重构后,我们都会执行上述测试代码,然后观察结果是否与之前的相同。当然如果你的是单元测试的话,完全可以把对结果检查的工作交给单元测试中的断言来做。 二、重构1:对较statement函数进行拆分 1.对statement()函数使用“Extract Method”原则 在上面的案例中,最不能容忍的,也就是最需要重构的首先就是Customer中的statement()函数。statement()函数最大缺点就是函数里边做的东西太多,我们第一步需要做的就是对其进行拆分。也就是使用我们之前提到过的“Extract Method”(提炼函数)原则对该函数进行简化和拆分。将statement()中可以独立出来的模块进行提取。经过分析后的,我们不难发现下方红框当中的代码是一个完整的模块,一个是进行单价计算的,一个是进行积分计算的,我们可以将这两块代码进行提取并封装成一个新的方法。在封装新方法时,要给这个新的方法名一个恰当的函数名,见名知意。 下方这块代码就是我们对上面这两个红框中的代码的提取。在提取时,将依赖于statement()函数中的数据作为新函数的参数即可。封装后的方法如下,在statement函数中相应的地方调用下方的方法即可。下方就是我们封装的计算当前电影金额和计算积分的函数。这两个函数都需要传入一个Rental的对象。 //根据租赁订单,计算当前电影的金额 func amountFor(aRental: Rental) -> Double { var result:Double = 0 //单价变量 switch aRental.movie.priceCode { case Movie.REGULAR: result += 2 if aRental.daysRented > 2 { result += Double(aRental.daysRented - 2) * 1.5 } case Movie.NEW_RELEASE: result += Double(aRental.daysRented * 3) case Movie.CHILDRENS: result += 1.5 if aRental.daysRented > 3 { result += Double(aRental.daysRented - 3) * 1.5 } default: break } return result } //计算当前电影的积分 func getFrequentRenterPoints(rental: Rental) -> Int { var frequentRenterPoints: Int = 0 //用户积分 frequentRenterPoints++ if rental.movie.priceCode == Movie.NEW_RELEASE && rental.daysRented > 1{ frequentRenterPoints++ } return frequentRenterPoints } 经过上面的重构步骤,我们会运行一下测试用例或者执行一下单元测试,看是否我们的重构过程引起了新的bug。 三、重构2:将相应的方法移到相应的类中 经过上面的重构,我们从statement()函数中提取了两个方法。观察这两个重构后的方法我们不难看出,这两个封装出来的新的方法都需要一个参数,这个参数就是Rental类的对象。也就是这两个方法都依赖于Rental类,而对该函数所在的当前类不太感冒。出现这种情况的原因就是这两个函数放错了地方,因为这两个函数放在Customer类中不依赖与Customer类而依赖于Rental类,那就足以说明这两个方法应该放在Rental类中。 经过我们简单的分析后,我们就可以决定要将我们新提取的方法放到Rental类中,并且函数的参数去掉。因为函数在Rental类中,所以在函数中直接使用self即可。将计算金额的方法和计算积分的方法移到Rental类中后,我们的Rental类如下所示。在我们的Customer中的statement()方法中在计算金额和计算积分时,直接调用Rental中的方法即可。经过这一步重构后,不要忘记执行一下你的测试用例,监测一下重构的结果是否正确。 四、使用“以查询取代临时变量”再次对statement()函数进行重构 经过第二步和第三步的重构后,Customer中的statement()函数如下所示。在计算每部电影的金额和积分时,我们调用的是Rental类的对象的相应的方法。下方的方法与我们第一部分的方法相比可谓是简洁了许多,而且易于理解与维护。 不过上面的代码仍然有重构的空间,举个例子,如果我们要将结果以HTML的形式进行组织的话,我们需要将上面的代码进行复制,然后修改result变量的文本组织方式即可。但是这样的话,其中的好多临时变量也需要被复制一份,这是完全相同的,这样就容易产生重复的代码。在这种情况下,我们需要使用“Replace Temp with Query”(已查询取代临时变量)的重构手法来取出上面红框中的临时变量。 上面红框中的每个临时变量我们都会提取出一个查询方法,下方是使用“Replace Temp with Query”(已查询取代临时变量)规则重构后的statement()函数,以及提取的两个查询函数。 经过上面这些步骤的重构,我们的测试用例依然不变。在每次重构后我们都需要调用上述的测试用例来检查重构是否产生了副作用。现在我们的类间的依赖关系没怎么发生变化,只是相应类中的方法有些变化。下方是现在代码所对应的类图,因为在上述重构的过程中我们主要做的是对函数的重构,也就是对函数进行提取,然后将提取的函数放到相应的类中,从下方的简化的类图中就可以看出来了。 五. 继续将相应的函数进行移动(Move Method) 对重构后的代码进行观察与分析,我们任然发现在Rental类中的getCharge()函数中的内容与getFrequentRenterPoints()函数中的内容对Movie类的依赖度更大。因为这两个函数都只用到了Rental类中的daysRented属性,而多次用到了Movie中的内容。因此我们需要将这两个函数中的内容移到Movie类中更为合适。所以我继续讲该部分内容进行移动。 移动的方法是保留Rental中这两个函数的声明,在Movie中创建相应的函数,将函数的内容移到Movie中后,再Rental中调用Movie中的方法。下方是我们经过这次重构后我们Movie类中的内容。其中红框中的内容是我们移过来的内容,而绿框中的参数需要从外界传入。 将相应的方法体移动Movie类中后,在Rental中我们需要对其进行调用。在调用相应的方法时传入相应的参数即可。下方就是经过这次中国Rental类的代码,绿框中的代码就是对Movie中新添加的方法的调用。 经过上面的重构,我们的方法似乎是找到了归宿了。重构就是这样,一步步来,不要着急,没动一步总是要向着好的方向发展。如果你从第一部分中的代码重构到第五部分,似乎有些困难。经过上面这些间接的过程,感觉也是挺愉快的蛮。下方是经过我们这次重构的类图。 六、使用“多态”取代条件表达式 在我们之前的博客中对条件表达式进行重构时,提到了使用类的多态对条件表达式进行重构。接下来我们就要使用该规则对Movie类中的getCharge()与getFrequentRenterPoints()函数进行重构。也就是使用我们设计模式中经常使用的“状态模式”。在该部分我们不需要对Rental类和Customer类进行修改,只对Movie类修改,并且引入相应的接口和继承关系。 我们对Movie类中的getCharge()方法中的Switch-Case结构观察时,我们很容易发现,此处完全可以使用类的多态来替代(具体请参见《代码重构(四):条件表达式重构规则(Swift版)》)。具体实现方式是将不通的价格计算方式提取到我们新创建的价格类中,每种电影都有自己价格类,而这些价格类都实现同一个接口,这样一来在Movie中就可以使用多态来获取价格了。积分的计算也是一样的。下方是我们要实现结构的类图。下方红框中是在原来基础上添加的新的接口和类,将条件表达式所处理的业务逻辑放在了我们新添加的类中。这样我们就可以使用类的多态了,而且遵循了“单一职责”。 下方代码就是上面大的红框中所对应的代码实现。Price是我们定义好的协议,在协议中规定了遵循该协议的类要实现的方法。而在每个具体实现类中实现了相同的接口,但是不同的类中相同的方法做的事情不同。在不同的类中的getCharge()中要做的事情就是Switch-Case语句中所处理的数据。 添加上上面的结构以后,在么我们的Movie中就可以使用多态了,在Movie中添加了一个Price声明的对象,我们会根据不同的priceCode来给price变量分配不同的对象。而在getCharge()中只管调用price的getCharge()函数即可,具体做法如下。 今天的博客到这儿也就差不多了,其实上面的代码仍然有重构的空间,如果我们想把Switch-Case这个结构去掉的话,我们可以在上面代码的基础上创建多个工厂方法即可。在此就不过赘述了。 如果看完今天的博客的内容不够直观的话,那么请放心。本篇博客中每次重构过程的完整实例会在github上进行分享。对每次重构的代码都进行了系统的整理。今天博客中的代码整理的结果如下。
在前段时间呢陆陆续续的更新了一系列关于重构的文章。在重构我们既有的代码时,往往会用到设计模式。在之前重构系列的博客中,我们在重构时用到了“工厂模式”、“策略模式”、“状态模式”等。当然在重构时,有的地方没有点明使用的是那种设计模式。从今天开始,我们就围绕着设计模式这个主题来讨论一下我们常用的设计模式,当然“GoF”的23种设计模式不会全部涉及到,会介绍一些常见的设计模式。在接下来我们要分享的设计模式这个系列博客中,还是以Swift语言为主来实现每种设计模式的Demo。并且仍然会在GitHub上进行Demo的分享,希望与大家相互交流,相互学习,有不足之处还望批评指正。 今天博客的主要思路是先围绕着“穿越火线”中的角色与武器的关系,通过策略模式来设计实现这种关系,整体的来整体感受一下“策略模式”的优点。然后再参考《Head First Design Patterns》这本书中的鸭子的示例,来一步步使用Swift来实现策略模式的案例。当然我们只是参考《Head First Design Patterns》中的示例,本篇博客中的示例与其中的示例还是有所区别的。大部分设计模式的案例都是使用Java实现的,我们依然会使用Swift来实现。还是那句话,设计模式是针对面向对象编程语言的,而不是针对某一种编程语言,Swift是面向对象的语言,所以设计模式用于Swift编程中是没有问题的。废话少说,进入今天博客的主题。 一、穿越火线中的“策略模式”(Strategy Pattern) 当然,这个示例是我YY出来的示例,不是“穿越火线”这个游戏的设计方案呢。说到"穿越火线"如果你没有玩过,那应该听过吧,就是“CrossFire”。我平时不怎么玩游戏,穿越火线之前体验过,不过只有被爆头的份儿。听说那些游戏玩家现在不怎么玩儿“CF”啦,改玩儿Dota,LOL啦,真的是这样吗?我个人对于游戏而言是外行了,不过玩个超级玛丽、魂斗罗、植物大战僵尸、节奏大师还是可以的(坏笑)。 言归正传,今天我们就模拟穿越火线中角色和武器的关系,使用“策略模式”来实现。首先我们先分析一下这个场景,穿越火线中角色分为不同的等级,也就是“军衔”了,简单的说几个吧,由高到底对应着“军师旅团营连排小工兵”,上面的是组织,军衔莫过于各种级的士官,少中上尉,少中上校,少中上将(应该对吧,本人不太专业呢,不过用于咱们要实现的例子是够了)。我虽然不怎么会打CF,可是我会玩军棋呢。 我是不是刷知乎刷多了,不能在这儿“一本正经的胡说八道”了。言归正传,不同的角色所配备的武器装备也不同,等级越高所使用的武器装备也就越厉害。我们如何使用面向对象来表达这种角色与武器之间的关系呢?我们先看一下下方的类“类图”。 上面是一个简化的类“类图”,上面这种形式可以表达我们之前的那种场景。“军人”是一个父类,其他具体等级的军官都继承自“SuperClass”。那么问题来了,在上面那种模式下,如果只有“少尉”和“中尉”配备某种武器,其他军官不配备,我们就要在“少尉”和中尉的类中分别添加要实现的武器,那么这样会产生冗余的代码。还有个问题是上面的设计形式不利于扩展,比如“少尉”也要配备狙击步枪,岂不是得从“中尉”中的狙击步枪的方法复制到“少尉”中。这样也会产生重复代码的。那么我们该怎样去解决这个问题呢? 有童鞋说了,在Swift中的Protocol(协议,也就是Java中的接口)可以提供默认的实现。也就是声明一个protocol,然后通过extension来为协议添加默认实现,只要是类遵循该协议,那么这个类就拥有了这个默认实现(当然,Java中的接口是不能通过后期的延展来为其添加默认实现的)。如果在Swift中使用接口的默认实现的话,如果要对上述军官扩充装备的话,设计中的类“类图”(不是类图,但与类图相似)实现如下所示: 上面这种设计模式虽然不会产生重复的代码,但是如果给“军官”添加的武器过多的话,那么会导致相应的类中实现的接口过多,这并不是我们想要的。下方将会给出一个良好的解决方案,也就是使用策略模式。 二、使用“策略模式”(Strategy Pattern)对上述关系进行设计 “策略模式”的定义大概是:策略模式,将不同的策略(算法)进行封装,让他们之间可以相互的替换,此模式让策略的变化独立于使用策略的用户。在设计模式中有不同的设计原则,其中有一条就是“找出程序中可能需要变化的地方,并且把它吗独立出来,不要和不变的代码混在一起”。根据这条设计原则,然后结合着上述示例不难分析出来,在上述示例中,使用军官使用的不同武器是可以变化的,使用不同的武器正是采取不同的策略呢。 所以经过上述讨论,我们可以使用“策略模式”来重新设计上面的结构。简单的说就是把变化的“武器”部分进行提取,然后在军官中进行使用,不同的军官可以采取不同的策略,并且可以随时替换。下面是我们使用“策略模式”重新设计后的关系,具体请看下图。 在上面的类“类图”中我们对可变的“武器策略进行了提取”。我们使用了WeaponBehavior协议来规定武器的策略,使得不同的武器对外有统一的接口,在此就是使用武器,也就是开火。不同的武器使用不同的的“开火策略”,但是对外的接口都是一样的。设计原则中有一条是“面向接口编程,而不是面向实现编程”。这里所指的接口可以是协议,可以是抽象类,也可以是超类,其实就是利用面向对象的“多态”特性。上面的红框中实现的就是所有不同的策略。 而绿框中是我们的用户,也就是军官的定义,是我们不变的部分。在军官中也有一个基类,在基类中定义了军官的共性,其中依赖于“武器策略”的接口。在军官超类中使用“武器策略”的协议声明了一个对象,该对象就是该军官所采取的武器策略。在军官的超类中可以通过setWeapon()方法采取不同的策略,其中fire()方法就是使用该“武器策略”进行开火。在具体的军官中的changeXXX()方法就是调用setWeapon()方法进行策略切换的方法。具体内容请看下方的具体实现。 三、上述“策略模式”(Strategy Pattern)的具体实现 上面给出了“武器策略模式”的个个部分之间的关系,并给出了相应的解释。如果对此你感觉到抽象的话,那么我们接下来就用相应的Swift代码去实现上述示例。也就是将上面的理论部分进行具体实现,当然在此我们用的是Swift语言,但是,你完全可以使用其他的面向对象编程语言。下面就是我们具体的代码实现。 下方就是我们对“武器策略”的实现,红框中对应的就是上面图中的WeaponBehavior(协议)接口,下方绿框中就是不同武器的策略,每个武器策略都遵循了WeaponBehavior协议。并且实现了相应的useWeapon()方法。 对“武器策略”模块实现完毕后,接下来我们就得实现军官模块了。也是根据上面我们所画的“模式结构图”来实现我们的“军官模块”,下方Character就是所有军官的基类,其中默认的武器策略weapon就是手枪(PistolBehavior),其中有设置策略和改变策略的方法,并且还有使用策略的方法(fire())。下方的红框就是实现的不同的军官了,不同的军官可以有不同的切换策略的方法。具体如下所示: 上面就是我们全部实现的代码,下方是我们的测试用例和输出结果。下方我们创建了一个“中尉”军官----lieutenant,军官默认的是开的手枪。但是可以调用相应的changeXXX()方法来切换武器策略。开手枪时,发现火力不行,然后就调用changeHK()方法切换到HK48步枪。这种关系使用“策略模式”就比较灵活,并且便于扩展。比如中尉现在也要配备大狙,因为现在已经有大狙这个武器策略了,所以我们现在只需在中尉中添加相应的change方法,传入大狙的武器策略即可,具体的就不在演示了。 本来想着在结合着《Head First Design Patterns》这本书中的鸭子示例在聊一下“策略模式”呢,由于篇幅有限,今天的博客到这儿吧。对于“策略模式”上面是一个完整的示例。
在之前发布Objective-C系列博客的时候,其中提到过OC的通知机制,请参考《Objective-C中的老板是这样发通知的(Notification)》这篇博客。在之前关于Notification的博客中,只介绍了Foundation框架中的通知的使用方式。正如前面博客中提到的那样,通知是“一对多的关系”,类似于广播。一个人发通知,多个人接收。这也就是设计模式中的“观察者模式”。接收者的一方是Observer(观察者),而发送方是Subject(主题)。一个人要想成为Observer,要在Subject中进行注册,也就是说要给Subject说,我要成为你的观察者,然后Subject就会给Observer推送消息。 我们不仅要知其然,还要知其所以然。今天博客的主题是“观察者模式”(Observe Pattern),所以我们要先通过一个小的Demo来理解一下“观察者模式” ,当然使用的是Swift语言来实现的(语言只是载体呢,主要还是模式不是)。通过一个小Demo对“观察者模式”进行学习后,紧接着会看一下在Swift中是如何使用Foundation框架中的通知的,并给出相应的示例。最后就是我们放大招的时候了,我们会参照着Foundation框架中的通知机制来实现我们自己的“通知中心”,说白了,就是我们不用Foundation的通知机制,我们自己写,但是使用方式与Foundation框架中的通知机制几乎相同。这应该就是Foundation框架中通知机制的实现原理吧。在本博文的开头需要有个干货预警呢。 一、认识“观察者模式”(Observe Pattern) 1.观察者模式的定义 开门见山,先来看一下观察者模式的定义吧: 观察者设计模式定义了对象间的一种一对多的依赖关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。 上面就是观察者模式的定义。也许你看定义有些抽象,其实观察者模式并不难理解。举个栗子,比如老板在一个办公室里开会,办公室里有部分员工,在办公室的员工就是Observer(观察者),正在开会的老板就是Subject(主题:负责发送通知---Post Notification)。如果其他员工也想成为Observer,那么必须得进入(addObserver)正在开会的会议室成为观察者。员工成功观察者后收到通知得做一些事情吧(doSomething),比如记个笔记神马的。如果此时员工闹情绪,不想听老板开会了,于是通过removeObserver走出了会议室。上面这个过程其实就是观察者模式。 2.从一个示例来认识“观察者模式” 上面描述了发通知的老板和接收通知的员工的观察者模式。接下来我们要用一个完整的示例来描述这个通知的过程,从一个完整的示例中来观察一下“观察者模式”的运作方式。当然场景还是使用Boss发送通知,员工接收通知的场景。这显然就是一对多的关系。 了解设计模式怎么会没有“类图”呢,当然在本篇博客以及本系列博客中使用的“类图”并不是真正的类图,只是看起来像类图,也就是类"类图"。但是类“类图”足以表示类间的各种关系。下方就是我们将要实现的“类图”。当然下方的的结构有很大的重构空间的,下方的基类完全可以使用protocol来实现的,但是为了简化结构我们用了简单的继承。但是下方示例是完全可以来表示“观察者模式”的。因为今天我们的主题是“设计模式”,其他关于重构的问题我们先不予理会。 下方SubjectType的基类就是通知者基类,负责发通知的,其中有表示发布消息的info: String字段,以及保存多个观察者的observerArray的数组(因为Subject :Observers 是1 对多的关系,我们在这儿使用数组类存储Observers)。在SubjectType类中还有三个方法,简单的说就是注册观察者(registerObserver)、移除观察者(removeObserver)、通知观察者(notifyObserver)这三个方法。Boss是SubjectType的子类,继承了SubjectType的所有属性以及要重写SubjectType中的三个方法,来完整要做的事情。在Boss中还有setInfo()方法,负责在更新Info信息的时候调用发出通知的方法。 ObserverType是观察者的基类,其中的info:String字段来存储接收到的通知信息。udpate()方法就是接收到通知后要执行的方法,也就是说Boss一发通知,员工就会执行update()方法,而其中的display()方法就是对上述信息进行输出。当然把SubjectType以及ObserverType做成基类,不利于我们后期的扩展或者在后期扩展中会产生重复的代码,使用使用接口或者结合者其他的设计模式可以很好的解决该问题。不过这不在于今天我们这篇博客的讨论范围之内,我们今天的重点是“观察者模式”。 3.上述“类图”的具体实现 原理在上述类“类图”中说的很明白了,接下来我们要通过上面的介绍来开始编写我们的代码,去实现上述“观察者模式”。上面的Boss负责发通知,Coder和PM负责监听Boss发的通知。下方就是我们的具体实现。 (1)ObserverType与SubjectType基类的实现如下图所示,这两个基类中的内容与上述“类图”中的描述一致。在SubjectType基类中的observesArray中存储的是ObserverType类型(包括其子类)的对象,也就是所有的观察者。 (2)下方就是我们负责发通知的大Boss。Boss继承自SubjectType,当Boss执行setInfo()方法时(也就是修改info的值时)就会调用notifyObservers()进行通知的发送。在Boss中的registerObserver()方法用来添加监听者(为了防止重复添加,我们在添加前先进行移除),removeObserver()则是负责移除监听者,notifyObservers()是发送通知并调用观察者相应的方法。具体实现如下所示: (3)下方实现的是两个观察者,分别是Coder(程序员)和PM(产品经理)。这两者都是ObserverType基类的子类,重写了ObserverType的update()和display()方法。观察者在观察到Subject的info被改变后,就会执行其中的update()方法。Coder和PM类的具体实现如下所示。 (4)经过上面这三小步,我们的Demo就实现完了,该到了我们测试的时候了,下方是测试用例以及输出结果。从输出结果我们不难看出,第一次发通知的时候,Coder和PM都接收到了通知,因为他们俩都是“观察者”。紧接着我们移除了Coder观察者,在发送第二次通知的时候,因为现在Coder不再是观察者了,所以第二次发送通知只有PM能收到。具体如下所示。 二、Foundation框架中的通知 在Foundation框架中是有一套完整的“观察者模式”机制的,也就是“通知机制”。在《Objective-C中的老板是这样发通知的(Notification)》这篇博客中就介绍了Foundation框架中的通知机制。本篇博客的第二部分就回顾一下Foundation框架中的“通知机制”,当然在本篇博客中我们会使用Swift来实现Foundation框架中的通知机制。 1. 简述NotificationCenter 在Foundation框架中的通知机制中有通知中心(NotificationCenter)这个概念,通知中心扮演者调度通知的作用。Subject往通知中心发送通知,由通知中心进行统一管理,把该Subject发送的消息分发给相应的观察者。可以这么说,通知中心是一个大集合,集合中有多个Subject和多个Observe的集合。而通知中心扮演的角色就是讲Subject与相应的Observer进行关联。下方就是简单的原理图了。 2.Foundation框架中的通知的使用 (1)创建Subject Foundation中自带的通知机制使用起来比较简单的,我们暂且将发送消息的称为Subject,通知的观察者称为Observer。下方是通知的Subject的实现,下方的Boss扮演的就是Subject角色。如果Boss要发送通知的话,需要下方几部: I. 创建消息字典,该字典承载的就是观察着说获取的信息。 II. 创建通知(NSNotification),该通知也是要发送给Observer的。通知中的信息量更大啊,其中包括发出通知的Subject的名字(每个Subject都有一个名字),还包括发送通知的对象,以及我们创建的消息字典。 III. 将该通知发送给“通知中心”----NotificationCenter,NotificationCenter会根据Notification所承载的信息来找到观察此通知的所有Observers,并把该Notification传给每个观察者。 下方就是Subject发送通知的具体做法。 (2)添加Observer 上面这一步是创建Subject,也就是往“通知中心”发送通知。接下来就是要往“通知中心”添加Observer,下方的代码就是往“通知中心”添加Observer。在添加Observer是,我们要指定该观察者所观察的是哪一个Subject。这也就是为什么要为Subject命名了,在添加Observer时就是通过Subject的名字来指定其观察的对象的。除了指定观察对象外,还需要指定收到通知后所执行的方法。在指定的方法中需要有一个参数,该参数就是用来接收上方Subject所发出的NSNotification的对象的。Observe的具体实现方式如下所示。 有一点需要注意的是,在当前对象释放时要移除观察者。 (3)测试用例 经过上面的两步,我们就已经使用Foundation框架中的通知机制将Subject和Observers进行了关联。接下来我们将对上方的代码进行测试,下方是我们的测试用例。测试用例灰常的简单了,在此就不做过多的赘述了。 三、照猫画虎:自定义通知中心 经过上面的部分,想必应该对“观察者模式”有所了解吧。经过上面的第二部分,你多Foundation中的通知机制使用是没有太大问题的。但是仅仅会使用不是我们想要的,还是那句话,要知其所以然。接下来我们就“照猫画虎,比葫芦画瓢”,自己实现一套专属自己的通知机制。在我们接下来要实现的通知机制中我们要根据Foundation框架中通知调用方式,来实现我们自己的通知。自定义通知的调用方式我们要做到与Foundation框架中的通知的使用方式一致,但是我们的命名是不同的。这部分才是今天博客的大招。 1.原理分析 我们先对Foundation框架中的通知机制进行观察,找一些灵感。当然我们看不到Foundation框架的源码,但是我们可以通过其对外暴露的接口来猜测其中通知的实现机制。下方是我们经过分析然后在经过推敲画出来的我们将要自己实现的通知机制的“类图”。我们也将根据下方的类图来实现属于我们自己的通知机制,“类图”如下。 下图中的MyCustomNotificationCenter就对应的NSNotificationCenter, MyCustomNotification则对应着NSNotification,而下方的MyObserver类与MySubject类在Foundation中对外应该是不可见的(这是个人猜测了),这两个类是为了实现该通知机制所创建的Subject和Observer。下方“通知机制”的运作方式就是Boss将Notification发送到NotificationCenter,然后NotificationCenter在通过其内部实现机制,将Boss发送过来的Notification发送到Coder。 在MyCustomNotification这个通知载体类中(类比NSNotification)的name字段表示发送通知的对象的名称,也就是上面的“Boss”, object字段就指的是上述示例的Boss的对象,userInfo就代表着发送给Observer的信息字典。MyObserver中存储的就是观察者对象(observe)和观察者对象收到通知后要执行的方法(selector)。 MySubject类扮演者“观察者模式”中的Subject,其中的notification字段记录着要发送的通知,其类型是MyCustomNotification。MySubject类中的observers是一个数组,其中存储的是该Subject对应的所有观察者。其中还分别有添加观察者(addCoustomObserver()), 移除观察者(removeCustomObserver()), 发送通知(postNotification())方法。具体如下“类图”所示。 中间的红框中的MyCustomNotificationCenter类,就是通知中心了(类比NSNotificationCenter), 该类的对象是通过defaultCenter()方法获取的单例。在该方法中有一个名为center的字段,center字段是字典类型,该字典的Key是我们为MySubject对象指定的name, Value是MySubject对象。其中也有移除、添加观察者,发送通知等方法。 2、Subject与Observer的代码实现 上面的原理也扯的够多了,接下来我们要根据上面的描述来使用Swift语言进行代码实现。还是直接上代码来的直观。在实现代码之前有一点需要声明的就是,该示例不能在Playground中实现,因为在Playground中执行performSelector()方法会抛出异常,所以我们需要在真正的工程中去实现(如果想简单一些,可以创建一个控制台程序来进行测试)。 (1). MyCustomNotification(类比NSNotification)具体实现 下方代码就是MyCustomNotification的具体实现了。通过下方的具体代码不难看出,name字段表示发送通知的对象的名称,也就是上面的“Boss”, object字段就指的是上述示例的Boss的对象,userInfo就代表着发送给Observer的信息字典。该类比较简单就不做过多赘述了。 (2). MyObserver的具体实现 下方代码就是MyObserver类的具体实现,该类还是比较简单的。MyObserver中存储的就是观察者对象(observer)和观察者对象收到通知后要执行的方法(selector)。当收到通知时,就会执行observer的selector方法。 (3). MySubject的实现 紧接着要实现我们的MySubject类了,MySubject类将Notification与Observers进行关联。具体说来就是当MySubject收到Notification中,就会遍历其所有的观察者(observers的类型是ObserveArray,其中存储的是MyObserver的对象),遍历观察者时就会去执行该观察者所对应的selector方法。下方的notification存储的就是Subject所要发出的通知。observers字段是数组类型,其中存储的是MyObserver的对象。addCustomObserver()方法其实就是往observers数组中添加观察者,而 removeCustomObserver()方法则是移除observers数组中的观察者。postNotification()方法的功能则是对observers数组进行遍历取出MyObserver的对象,然后执行该对象中的selector方法,并且将notification作为selector方法的参数。具体实现如下所示。 3.“通知中心”的代码实现 上面实现的是Notification、Subject以及Observer的代码的实现,接下来要实现“通知中心”模块。因为该模块的代码比较多,业务逻辑相对复杂,所以我想把这部分代码进行拆分,然后各个击破。下方截图是MyCustomNotificationCenter类的定义,我们先将类中的代码折叠,然后将折叠的代码进行拆分各个击破。下方是通知中心MyCustomNotificationCenter类的定义方式。 (1)在MyCustomNotificationCenter类中我们也模拟NSNotificationCenter的defaultCenter()方法来获取该类的单例,具体代码如下所示。下方我们将其构造器声明为私有,防止其在外部进行实例化。然后使用静态方法defaultCenter()来返回一个当前类的静态实例,下方就是Swift中比较简单的“单例模式”了。 (2)、下方的的方法就是通知中心发送通知的过程了,对应着NSNotificationCenter.defaultCenter()中的postNotification(notifaction)。我们要实现postNotification()方法也有一个参数,该参数就是Subject要发送的通知。在postNotification()方法中,首先会调用getSubjectWithNotifaction(notification)方法来从center中获取可以发送该notification的Subject对象。在getSubjectWithNotifaction(notification)中,如果center中没有可以发送该notification的对象,那么就创建一个MySubject对象,并将该notification赋值给这个新建的MySubject对象,最后将我们创建的这个新的subject添加进center数组中。然后调用该subject对象的postNotification()方法即可,具体实现如下所示。 (3)下方代码就是添加监听着了,与NSNotificationCenter.defaultCenter()中的addObserver()方法相对应。首先我们把传入的参数生成MyObserver的对象,然后通过aName从center字典中获取相应的MySubject对象。如果center中没有对应的MySubject对象,我们就创建该对象,并且将该对象的notification属性暂且指定为nil。最后调用MySubject类中的addCustomObserver()方法进行观察者的添加。 (4) 下方代码就比较简单了,就是移除观察者。首先也是通过name从center字典中获取MySubject的对象,然后调用MySubject对象的removeCustomObserver()方法进行移除掉。具体代码如下所示。 4.测试用例 经过上面的艰苦跋涉,我们自己定义的通知机制终于完成了。下方就是我们为上述自定义通知机制所创建的测试用例。将下方的测试用例与Foundation框架中的通知机制的测试用例(本篇博客第二部分)相比是非常相似的。至此我们自定义的通知就Over了,这也就是Foundation框架中通知机制实现的大概原理吧,当然Foundation框架还对其做了各种优化。但是万变不离其宗,都是“观察者模式”的应用。 下方是我们自定义通知的测试用例,是在本篇博客中第二部分的代码的基础上进行修改单,就是Foundation框架中的通知进行了替换。具体如下所示: 上面是在Swift2.1版本中实现的代码,在Swift2.2中的Selector的参数有所变化,在此还是需要说明一下的,aSelector参数在Swift2.2中得使用#selector(类.方法),如下所示: 如果你对本篇博客的内容从头到尾的进行阅读,并且将上面的实例用自己熟悉的一门语言来实现的话,想必你对“观察者模式”更进一步的了解了吧。
为什么要搞一搞SQLite的C语言接口规范呢? 因为在做iOS开发中难免会遇到操作数据库的情况,你可以使用第三方的FMDB等,或者使用CoreData。但我们还是有必要去搞清楚如何去使用SQLite的C语言接口来操作SQLite数据库的。从今天开始就给大家结合实例详细的搞一搞SQLite的C语言接口。关于CoreData的东西请看之前的博客《IOS开发之表视图爱上CoreData》。 如果英文好的小伙伴呢,你可以不听我啰嗦,直接官网走起:http://www.sqlite.org 上面的东西是应有尽有,你可以下载资源如SQLite的Shell, 上面还有好多的学习资源。不过前提是英文不能太Low呢。之前看过几本iOS开发的书籍,也包括某某出版社出版的《精通iOS开发》,虽然网上评价不错,但看书的时候总是不来感。大部分书上介绍的SQLite, 讲的太浅,只是罗列代码,接口参数是什么意思,为什么这么写都没讲。看书看的不爽了,就到官网上找找安慰吧,果不其然,眼前一亮。就写几篇博客好好的总结一下。 一、准备SQLite测试工程和所需工具 1. 准备一个已经引入动态链接库libsqlite3.0.dylib的iOS单视图工程(当然,看你心情,你也可以创建一个控制台工程,这不是重点)。 2. 准备一个SQLite可视化的管理工具,我用的是SQLiteManager, 当然你可以选择你用着顺手的管理工具(自行百度吧)。当然如果你是初学者,并想“自残”一下话,可以从官网上Download一个叫做sqlite-shell的东西,用纯命令行去管理你的SQLite数据库。其实如果习惯了,用纯命令还是用着比较爽的,毕竟可以用来装13不是么! SQLite官网上有详细的Shell操作命令:如何去创建数据库,如何创建表等一系列的操作,今天不做赘述。(如果你之前搞过MySQL, Oracle等,应该对命令行操作数据库再熟悉不过了)。 3. 你可以通过SQLiteManager来创建一个数据库插入一些测试数据,以备在我们的测试工程中进行使用。或者你可以懒一些,直接从网上Download一个现成的SQLite数据库进行操作使用(我下载了一个叫做Cars.sqlite文件来进行测试,数据库的表结构及数据如下所示)。 二、打开你的数据库 1.把准备好的测试SQLite数据库引入到我们的测试工程中。 2.通过NSBundle加载我们的数据库资源 //获取Sqllite文件的路径 NSString *sqlPath = [[NSBundle mainBundle] pathForResource:@"Cars" ofType:@"sqlite"]; 3.因为是C语言接口,参数所用的字符串都是C语言中的字符串,所以呢得把字符串转成C语言中的字符串吧(也就是C语言中char类型的指针) //把路径转成C字符串 const char * filePath = [sqlPath UTF8String]; 4.你需要定义一个sqlite3结构体类型的指针变量,打开数据库后可以获取这个sqlite3结构体指针的值,并赋值给之前对应的指针变量,然后就可以通过该sqlite3结构体指针变量来操作数据库。下面定义了一个sqlite3结构体类型的指针变量,然后把该指针变量的地址传给sqlite3_open()函数,函数参数传入的引用,在C语言中就可以得到数据库操作指针。为了便于理解,可以把sqlite3结构体当做一个类,而sqlite3结构体的指针可以看做是类的对象。 sqlite3 * database; //打开数据库 int result = sqlite3_open(filePath, &database); 通过上述步骤就可以获取到操作数据库的结构体指针,sqlite3_open()函数,第一个参数就是C字符串格式的数据库文件的路径,第二个参数就是结构体指针的地址,用于获取操作数据库的句柄。该函数有一个int类型的返回值(0-101),这些返回值对应着不同的链接状态。0代表着成功,其余见下图:if (result == SQLITE_OK) { NSLog(@"连接成功"); } else { NSString *error = [NSString stringWithFormat:@"错误结果代码:%d", result]; NSLog(@"%@", error); } sqlite3_open()就是一个构造函数, 另外还有sqlite3_open16()和sqlite3_open_v2(), 他们的功能都是打开一个新的数据库的连接,所需参数如下所示。这些构造函数可以通过数据库文件名称参数来连接一个数据库。如果文件名参数是UTF-8编码格式的, 可以调用sqlite3_open()和sqlite3_open_v2(), 那么如果文件参数是 UTF-16编码的话就调用构造函数sqlite3_open16()。第二个参数就是返回的数据库操作句柄的指针地址。 由下方的图可以看出sqlite3_open_v2()比sqlite3_open()多了两个参数,一个是int flags, 一个是const char *zVfs。 sqlite3_open_v2()的用法和sqlite3_open()类似,可以说前者是后者的加强版。sqlite3_open()是以前的旧方法,而sqlite3_open_v2()是后来改进的方法。 参数flag,不同的值代表着打开数据库后可以获取的不同操作,类似于数据库的操作权限,下方是flag的值代表的操作权限。 SQLITE_OPEN_READONLY 数据库是只读模式打开。如果数据库不存在,则返回一个错误。 SQLITE_OPEN_READWRITE 数据库以读写的模式打开, 如果文件被操作系统设置为保护模式,那么就为只读模式。在这两种情况下的数据库必须已经存在,否则会返回一个错误。 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE 数据库以读写的模式打开, 如果数据库不存在,就创建一个。使用sqlite3_open()和sqlite3_open16()连接数据库时,默认的就是这种行为。 如果sqlite3_open_v2()的第三个参数不包含上述三种结合中的一个的话,那么数据库的连接权限是未定义的。也就是说数据库不知道是读还是写,还是创建,所以操作数据库就没有意义了,所以上面必须选择一个参与“与”运算。 SQLITE_OPEN_NOMUTEX 只要单线模式下没有设置编译的起始时间,就会在多线程模式下进行数据库的连接。 SQLITE_OPEN_FULLMUTEX 在序列化的线程模式(在此模式中,SQLite能无约束地在多线程中安全使用)打开数据库连接,除非在编译时或者单线程之前选择起始时间。 SQLITE_OPEN_SHAREDCACHE 可以使数据库连接适当的使用共享缓存模式,无论是否使用sqlite3_enable_shared_cache()启用共享缓存。 SQLITE_OPEN_PRIVATECACHE 导致数据库连接不使用共享缓存模式,即使共享缓存模型可用。 sqlite3_open_v2()第四个参数是sqlite3_vfs对象的名称,它定义了操作系统接口应该使用新的数据库连接。如果第四个参数是一个nil的话,那么就会使用默认sqlite3_vfs对象。下方是结构体sqlite3_vfs的具体内容: vfs: sqlite3_vfs对象的实例定义了一个SQLite核心和底层操作系统间的接口。“vfs”对象的名称代表“虚拟文件系统”。关于VFS的详解内容在这里:https://www.sqlite.org/vfs.html 有兴趣的小伙伴可以好好的搞一下。如果以后有时间的话在好好的介绍一下VFS。今天就不做过多的赘述了。第四个参数传入nil就会使用默认的sqlite3_vfs默认对象。 关于VFS和sqlite3_vfs结构体的东西,如果以后有时间,在单独拿出来搞搞。了解VFS的结构和模式还是很有必要的。ok~今天打开并连接数据库,关于如何去通过接口去操作数据库就留在以后的博客中介绍吧。 用到的数据库和sqliteAPI代码GitHub分享地址:https://github.com/lizelu/SQLiteResource 在博客的最后呢,给出简单封装的打开数据库的方法: /******************************* *功能:打开数据库 *参数:databaseName -- 数据库名称 *返回:数据库对象(sqlite3对象) *******************************/ + (sqlite3 *) openDatabaseWithName: (NSString *)databaseName{ //获取Sqllite文件的路径 NSString *sqlPath = [[NSBundle mainBundle] pathForResource:databaseName ofType:@"sqlite"]; //把路径转成C字符串 const char * filePath = [sqlPath UTF8String]; sqlite3 * database; //打开数据库 int result = sqlite3_open(filePath, &database); if (result == SQLITE_OK) { return database; } return nil; }
在《SQLite的C语言接口规范(一)》中介绍了如何去连接打开数据库,本篇博客就介绍如何操作数据库,本篇主要给出了如何执行数据库查询语句(Select), 然后遍历结果集。本篇博客就直接使用上一篇博客封装的打开数据库的方法获取到数据库的操作句柄,然后通过这个句柄来操作我们的Sqlite数据库。今天这篇博客中要多Cars.sqlite数据库中的其中一个表进行Select操作。更为细节的东西请参考SQLite官网:http://www.sqlite.org 。 一.预编译SQL语句 要想执行一条查询的SQL语句,需要使用下面任何一个方法先预编译成字节码程序。不难看出以下方法的参数都是一样的,那么就先挨个的介绍一下每个参数的代表什么。 1. 参数“sqlite3 * db”, 就是我们调用sqlite3_open(), sqlite3_open_v2() 或者 sqlite3_open16()成功后获取的操作数据库的句柄。数据库连接必须没有被关闭。 2. zSql是第二个参数, 他的编码格式是UTF-8或UTF-16, 它就是将会被预先编译成字节码的SQL语句。sqlite3_prepare() 和 sqlite3_prepare_v2()接口使用的是UTF-8编码。sqlite3_prepare16() 和 sqlite3_prepare16_v2() 使用的是 UTF-16编码。 3. nByte是第三个参数,说白了,它就是参数zSql字符串的最大长度。如果nByte是负数,那么zSql的长度不限,如果nByte是正数,zSql的长度则不能超过nByte的数值,超出的部分将不会被预编译。如果nByte是0,那么zSql将不会被预编译。如果你之前学过C语言的话,在C语言中是没有所谓的字符串的,是一个指向字符的指针,后面跟了好多字符,以‘\0’结尾,这就是C语言中的字符串,需要通过指针的移动来遍历字符串的,所以nByte是很有必要的。 4. *ppStmt 是预编译语句后左边的指针,它可以使用sqlite3_step()执行。在发生错误时,*ppStmt就会被设置为NULL。如果输入的文本不是SQL语句(输入的文本为空字符串或者一行注释)*ppStmt就会被设置为NULL。 sqlite3_finalize()负责释放被编译的SQL语句。 5. pzTail, 看pzTail的类型就可以看出它是指向指针的指针。pzTail指向谁的指针呢?如他不为NULL的话,它就指向预编译SQL语句的末尾,也就是未预编译SQL语句的首指针。 二、预编译SQL语句实例 下面是使用sqlite3_prepare()来预编译的一条查询语句,在新的项目中建议使用sqlite_prepare_v2(), 他是前者的升级版。v2代表什么意思,在上一篇博客中进行的简单介绍,以后如果有时间,会对VFS(虚拟文件系统)进行详细的介绍。 1.定义NSString类型的SQL查询语句,如下所示: //查询数据库 NSString * qureyInfo = @"SELECT * FROM CARBRAND"; 2. 定义sqlite3_stmt变量来接受预编译后的语句。 sqlite3_stmt *statement; 3.把NSString类型 SQL语句转成UTF-8的类型。 const char * zSql = [qureyInfo UTF8String]; 4.调用sqlite3_prepare()进行预编译,sqlite3_prepare()预编译后会有结果状态码,在上一篇博客中进行了讲解,本篇博客不做过的赘述: int result = sqlite3_prepare(database, zSql, -1, &statement, nil); 经过上面这些步骤就可以获取到预编译后的SQL语句statement,然后我们就可以通过statement做一些爱做的事情了。 三、执行预编译后的SQL语句 执行预编译后的SQL语句需要调用sqlite3_step()。 sqlite3_step() 会被一次或多次执行,由下方截图可知,sqlite3_step()的参数就是预编译后的语句的指针(sqlite3_stmt *)。在新的项目中推荐使用sqlite3_prepare_v2()和sqlite3_prepare16_v2()。因为要向后兼容,所以之前的接口进行了保留,不过,不建议使用sqlite3_prepare()和sqlite3_prepare16()。在“v2”接口中,被返回的预编译语句(sqlite3_stmt对象)包含了一个原始SQL语句的副本。这导致了sqlite3_step()有三种不同的表现形式。 1.如果数据库的Schema发生变化了,之前会返回SQLITE_SCHEMA,如果使用带v2的方法的话,sqlite3_step()将自动重新编译SQL语句并再次尝试运行它。因为使用v2的方法,预编译的结果中将包含SQL原始语句。 2.当错误发生时,sqlite3_step()将会返回更为详细的错误代码和扩展错误代码。而之前的做法是返回一个通用的错误结果代码SQLITE_ERROR,而你不得不去调用sqlite3_reset()方法来查找问题。在“v2”预编译接口中将会立即返回错误原因。 3.如果特定的值与WHERE子句中的条件进行绑定,这就会影响查询结果,这个语句将会自动被重新编译,类似于数据库的架构改变的情况。 下方是扩展后的结果集: 上面说这么多,就是一句话,在预编译时强烈推荐使用“v2”预编译接口,“v2”预编译接口是升级版,功能更强大。 sqlite3_step()接口去执行预编译后的语句,也会返回一些结果代码,下面介绍一些常用的结果代码:SQLITE_BUSY, SQLITE_DONE, SQLITE_ROW, SQLITE_ERROR或者 SQLITE_MISUSE,如果你是使用的“v2”接口进行编译的话,将会返回更多更为详细的结果编码。 SQLITE_BUSY 意味着数据库引擎无法获得所需的数据库锁然后做它的工作。如果语句是commit或执行一个外部的显式事务,你可以重试。如果的语句不是提交并且执行一个内部显示的事务,那么在重试之前你应该回滚事务。 SQLITE_DONE 意味着语句执行完成并且成功。没有初次调用sqlite3_reset()来重置虚拟机恢复到初始状态,sqlite3_step()就不应该再调用这个虚拟机。 SQLITE_ROW 如果正在执行的SQL语句返回任何数据, 为了便于调用者处理,如果有数据,返回结果就是SQLITE_ROW。再次sqlite3_step()来检索数据的下一行。 SQLITE_ERROR 出错的状态,你可以调用sqlite3_errmsg()来查看具体的错误。sqlite3_errmsg()所需参数和返回值 上面已经准备好了预编译好的SQL语句,我们使用sqlite3_step()来执行和遍历一下结果集,具体代码如下: if (result == SQLITE_OK) { NSLog(@"查询成功"); //遍历结果集 while (sqlite3_step(statement) == SQLITE_ROW) { int rowNum = sqlite3_column_int(statement, 0); char *rowDataOne = (char *) sqlite3_column_text(statement, 1); char *rowDataTow = (char *) sqlite3_column_text(statement, 2); NSString *nameString = [NSString stringWithUTF8String:rowDataOne]; NSString *firstLetterString = [NSString stringWithUTF8String:rowDataTow]; NSLog(@"BrandId = %d, Name = %@, FirstLetter = %@",rowNum , nameString, firstLetterString); } sqlite3_finalize(statement); } else { NSString *error = [NSString stringWithFormat:@"错误结果代码:%d", result]; NSLog(@"%@", error); } 可以对上面的代码进行一下修改,在while循环语句的最后一条语句后,加上sqllite3_reset(), 那么这个循环就是一个死循环,读取的永远是第一个数据。在这儿就不往上贴代码了。 执行结果如下: 好,今天的数据库查询就先到这儿,关于别的内容,会在下节博客中进行介绍。
在前面的博客中已经介绍了如何连接SQLite数据库,并且简单的查询和遍历结果集。在前面用到了sqlite3_stmt *stmt,也就是预编译后的SQL语句。在本篇博客中会了解一下sqlite3_stmt,然后了解一下变量的绑定。变量绑定,简单的说就是往预编译后的SQL语句中传入相应的值。 一. sqlite3_stmt 的生命周期 这个对象的实例代表着一个被编译成二进制的SQL语句。每个SQL语句都必须经过预编译转换成sqlite3_stmt才能被执行。在iOS开发中,Application或者UIViewController等都是有自己的生命周期的,预编译语句也是有自己的生命周期的,其生命周期如下: 1.sqlite3_stmt对象的生命起源于sqlite3_prepare_v2(), 使用sqlite3_prepare_v2()可以创建sqlite3_stmt对象。 2.使用sqlite3_bind_*()接口可以给sqlite3_stmt对象绑定变量。 3.调用sqlite3_step()一次或者多次来运行SQL语句。 4.调用sqlite3_reset()回到上一次执行的SQL语句,你可以调用sqlite3_reset()多次,sqlite3_reset()更像版本管理中的回滚操作。 5.使用sqlite3_finalize()析构函数来释放sqlite3_stmt对象。 sqlite3_stmt对象的构造函数(推荐使用“v2”接口):sqlite3_prepare(), sqlite3_prepare16(), sqlite3_prepare16_v2(),sqlite3_prepare_v2() sqlite3_stmt对象的析构函数:sqlite3_finalize() 下方是有关sqlite3_stmt对象的接口,本篇博客主要介绍有关sqlite3_bind_*()的方法。 二、值绑定 先简单介绍一下什么是值绑定吧。用大白话说,值绑定就是在SQL语句预编译时把一些参数使用占位符(这里是?号)来代替,然后与程序中的变量进行绑定。类似于字符串的格式化。如果你之前搞过Java的JDBC或者PHP, 在他们操作数据库执行SQL语句时都有类似值绑定的一个概念。 就是把外界变量把SQL语句占位的值进行替换。值绑定经常在SELECT,INSERT,UPDATE等语句中进行使用。 1.为预编译SQL语句绑定变量,绑定不同类型变量需要不同的绑定接口,下方是常用的绑定变量的接口。 2.在sqlite3_prepare_v2()输入的SQL语句的文本中,下面的这些参数将会被替换掉,在下面的参数中,NNN表示一个整数(这个整数就代表这个参数的索引),VVV代表一个字母标示符(参数的名字)。可以使用sqlite3_bind_*()函数为上面的这些占位符进行赋值。 说的直白一些,“?”号就是匿名参数,从第一个问号出现往后的索引默认是1,往后以此类推。而“?NNN”是为匿名参数指定索引,你可以这样写“?1” , "?2"等,而:VVV, @VVV, $VVV这些就是有名参数了,VVV就是参数的名字。比如:ludashi, @ludashi, $ludashi。 下面的实例给出了参数不同的几种表现形式, 前一种是匿名参数,后边参数就有自己的名字了。 3.sqlite3_bind_*()参数介绍(这些绑定函数执行成功后回返回SQLITE_OK, 执行不成功的话回返回相应的错误代码) (1) sqlite3_bind_*()的第一个参数是含有上述占位符预编译后的语句指针,也就是sqlite3_stmt的对象。 (2) sqlite3_bind_*()的第二个参数是SQL语句中参数的索引,例如第一个参数的索引值是1,那么就传1。匿名参数的索引是从1开始往后递增的,而有参数名称的可以通过sqlite3_bind_parameter_index()接口传入参数名称来获取该参数的索引,sqlite3_bind_parameter_index()用法如下,第一个参数是sqlite3_stmt的对象,而后边的参数是SQL参数名称,返回值就是该参数的索引。 int index = sqlite3_bind_parameter_index(statement, "$brandidMin"); (3) 第三个参数是要绑定的值。 (4) sqlite3_bind_blob(), sqlite3_bind_text()和sqlite3_bind_text16()这三个接口中还有有第四和第五个参数,第四个参数代表第三个参数“绑定值”的字节长度。第五个参数是一个指向内存管理回调函数的指针。 4.每个绑定函数的使用场景 (1) BLOB是数据库中存储大数据的一种数据类型,它是以二进制的形式来存储数据的。 1 SQLITE_API int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*)); (2) 顾名思义,下面的方法是绑定double类型的数据的 1 SQLITE_API int sqlite3_bind_double(sqlite3_stmt*, int, double); (3) 绑定一个32位的整型值 1 SQLITE_API int sqlite3_bind_int(sqlite3_stmt*, int, int); (4) 绑定一个64位的整型值 1 SQLITE_API int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64); (5)绑定一个NULL的值(在数据库中可以为NULL) 1 SQLITE_API int sqlite3_bind_null(sqlite3_stmt*, int); (6)绑定一个UTF-8编码的字符串,第四个参数上面也提到了,是绑定字符串的长度,如果为负值的话,就是传多少就绑定多少。 1 SQLITE_API int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*)); (7)绑定一个UTF-16编码的字符串,第四个参数上面也提到了,是绑定字符串的长度,如果为负值的话,就是传多少就绑定多少。 1 SQLITE_API int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int, void(*)(void*)); (8) 绑定sqlite3_value结构体类型的值,sqlite3_value结构体可以保存任意格式的数据。 SQLITE_API int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*); (9)绑定一个任意长度的BLOB类型的二进制数据,它的每一个字节被置0。第3个参数是字节长度。这个函数的特殊用处是,创建一个大的BLOB对象,之后可以通过BLOB接口函数进行更新。1 SQLITE_API int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n); 5.值绑定常用工具函数 (1)下方的函数返回预编译SQL语句中参数的个数,这些参数可以是匿名参数,也可以是有名参数。 具体用法如下: 1 int count = sqlite3_bind_parameter_count(statement); 2 NSLog(@"%d", count); (2)通过索引获取对应参数的名称 具体用法如下: 1 const char * name = sqlite3_bind_parameter_name(statement, 1); 2 NSLog(@"%s", name); (3) 在一个是通过名字获取索引了,正好和上面的方法相反。 调用方式如下: 1 int index = sqlite3_bind_parameter_index(statement, ":brandidMax"); 2 NSLog(@":brandidMax——index = %d", index); 三、值绑定实例 下面的实例是在查询语句中使用值绑定,绑定完值后,调用查询数据库的方法,然后进行数值的输出,因为上面说的够多了,下面的代码就不用加注释了。 -(void) qureyInfoWithDataBase2: (sqlite3 *) database{ NSString * qureyInfo = @"SELECT * FROM CARBRAND WHERE BRANDID > :brandidMin AND BRANDID < :brandidMax"; sqlite3_stmt *statement; const char * zSql = [qureyInfo UTF8String]; int result = sqlite3_prepare_v2(database, zSql, -1, &statement, nil); int count = sqlite3_bind_parameter_count(statement); NSLog(@"count = %d", count); const char * name = sqlite3_bind_parameter_name(statement, 1); NSLog(@"name = %s", name); if (result == SQLITE_OK) { int index = sqlite3_bind_parameter_index(statement, ":brandidMax"); NSLog(@":brandidMax_index = %d", index); //值绑定 sqlite3_bind_int(statement, 1, 180); sqlite3_bind_int(statement, 2, 200); [self queryUserInfoWith: database WithStatement: statement]; } } 查询数据库的方法 //查询数据库 - (void) queryUserInfoWith: (sqlite3 *) database WithStatement: (sqlite3_stmt *) statement { while (sqlite3_step(statement) == SQLITE_ROW) { int rowNum = sqlite3_column_int(statement, 0); char *rowDataOne = (char *) sqlite3_column_text(statement, 1); char *rowDataTow = (char *) sqlite3_column_text(statement, 2); NSString *nameString = [NSString stringWithUTF8String:rowDataOne]; NSString *firstLetterString = [NSString stringWithUTF8String:rowDataTow]; NSLog(@"BrandId = %d, Name = %@, FirstLetter = %@",rowNum , nameString, firstLetterString); } sqlite3_finalize(statement); } 输入结果如下: 今天博客就先到这儿,关于SQLite数据库的东西会继续更新。
数据库的在上一篇博客中《SQLite之C语言接口规范(三)——Binding Values To Prepared Statements》用到了如何从查询结果中取出结果值。今天的博客就详细的介绍一下sqlite3_column_*()的方法。在SQLite数据库C语言接口中,从查询结果中取出不同类型的值需要不同的接口函数。 一. sqlite3_column_*()介绍 1.下图是sqlite3_column_*()所包含的方法,由下图容易的看出取出不同类型的值需要不同的接口函数。可以取出的类型有blob, bytes, bytes16, double, int, int64, text, text16等。接口的第一个参数是我们预编译的SQL语句(sqlite3_stmt的对象),第二个参数是要取出值得行数(从左往右,起始于0)。上面这些接口返回的信息是当前查询行中某列的值。在所有情况下,第一个参数确切的说是指向预编译语句(由sqlite3_prepare_v2() 函数返回的 sqlite3_stmt *)的指针。 第二个参数是应该返回信息在行中的列索引(结果集的最左边的列索引0)。结果集中的列的数量可以使用sqlite3_column_count()来获取。 如果SQL语句目前并不指向一个有效的行或列索引超出了范围内,那么结果集是未定义的。上面这些方法仅仅在调用sqlite3_step()函数并且返回SQLITE_ROW的情况下调用,不能在sqlite3_reset()和sqlite3_finalize()执行后调用上述方法。如果你这样做了,结果集将是不确定的。 2. sqlite3_column_count()具体使用方法如下, 其参数就是sqlite3_stms *的预编译语句的指针, 返回值就是当前结果集的列数。 //获取查询结果所有的行数 int columnCount = sqlite3_column_count(statement); NSLog(@"columnCount = %d", columnCount); //columnCount = 4 3. sqlite3_column_type()这个函数会返回相应列上数据的类型代码。返回的结果是SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB 或者 SQLITE_NULL 其中一种情况。在API中对应接口的宏定义如下。 sqlite3_column_type()的调用必须放在sqlite3_step()函数执行(并且有结果返回),不然就会返回NULL。使用方式如下: 1 int columnType = sqlite3_column_type(statement, 1); 2 3 NSLog(@"columnType = %d", columnType); //columnType = 3(SQLITE_TEXT) 4. 如果查询结果的类型是 BLOB 或者 UTF-8 字符串类型,你可以使用sqlite3_column_bytes()方法来获取该数据的字节长度。如果结果是UTF-16的字符串,sqlite3_column_bytes()方法将会把字符串自动转成UTF-8的字符串类型,然后再返回字符串的字节数。 sqlite3_column_bytes16()用法是获取UTF-16字符串数值所占字节数的,用法和 sqlite3_column_bytes8()相同。这两个方法返回的不是字符串的字符个数,而是字符串所占字节的个数,当然所占字节的个数在这儿不包括C语言中字符串结尾的“\0”。 该函数的具体用法如下: 1 int currentValueBytes = sqlite3_column_bytes(statement, 2); 2 3 NSLog(@"%@字节数为 = %d", firstLetterString, currentValueBytes); 5. sqlite3_column_value()返回的是一个不受保护的sqlite3_value对象。在多线程环境下,一个不受保护的sqlite3_value对象,只有被 sqlite3_bind_value() 和 sqlite3_result_value()接口使用才是安全的。 二、使用实例 在上一篇博客中的查询遍历的方法中进行扩充,扩充后的方法如下: //查询数据库 - (void) queryUserInfoWith: (sqlite3 *) database WithStatement: (sqlite3_stmt *) statement { //获取查询结果所有的行数 int columnCount = sqlite3_column_count(statement); NSLog(@"columnCount = %d", columnCount); while (sqlite3_step(statement) == SQLITE_ROW) { int rowNum = sqlite3_column_int(statement, 0); char *rowDataOne = (char *) sqlite3_column_text(statement, 1); char *rowDataTow = (char *) sqlite3_column_text(statement, 2); NSString *nameString = [NSString stringWithUTF8String:rowDataOne]; NSString *firstLetterString = [NSString stringWithUTF8String:rowDataTow]; NSLog(@"BrandId = %d, Name = %@, FirstLetter = %@",rowNum , nameString, firstLetterString); int columnType = sqlite3_column_type(statement, 1); NSLog(@"\"%@\" 类型代码为 = %d", nameString, columnType); //columnType = 3(SQLITE_TEXT) int currentValueBytes = sqlite3_column_bytes(statement, 1); NSLog(@"\"%@\" 字节数为 = %d \n\n", nameString, currentValueBytes); } sqlite3_finalize(statement); } 调用上面的方法,具体的输入结果如下: 今天的内容就先到这儿,下篇博客回来一个完整的实例,把SQL的增删改查的方法进行封装,对数据库进行操作。下篇博客用到的数据库就不能放到Bundle中了,需要把其拷贝到沙盒中,然后再对其进行增删改查。具体内容详见下篇博客(稍后更新)。
本篇博客就使用前面操作SQLite的知识来实现如何去插入,删除和更新数据。然后再把操作SQlite数据库常用的方法进行一个封装。把常用方法进行封装后,把Cars数据库中的其中一个表的数据进行查询,并在UITableView上进行展示。因为本实例要对数据库的数据进行modify(修改)操作 ,在iOS系统上呢,为了安全起见,在Bundle中的数据库资源是不允许进行数据的插入修改和删除操作的。在之前的博客中我们只进行了查询操作,所以从Bundle加载数据库资源文件是可行的。 如果对数据库进程insert, update, delete等操作,那么需要在打开数据库之前把Bundle中的数据库拷贝到沙盒中(每个App都有自己的沙盒,在没有越狱的机器上,App只可以访问自己的沙盒,这也是iOS比较安全的地方之一)。今天这篇博客会封装出一个操作SQLite数据库的工具类,并且调用这个工具类对数据库进行增删改查,实现一个小的实例。废话少说,直奔主题。 一、数据库操作工具类 为了操作数据库更为方便,对数据库操作:打开关闭数据库,无绑定值查询数据库,有绑定值查询数据库,插入数据,删除数据,更新数据等进行了简单的封装。当然有感兴趣的小伙伴可以继续完善,比如加上事务操作等。 工具类对外接口介绍 先来看一下封装的工具类对外的接口,然后介绍一下其使用方法。接口代码具体如下所示 // // OperationSqliteTools.h // SettingBundleDemo // // Created by Mr.LuDashi on 15/8/31. // Copyright (c) 2015年 zeluli. All rights reserved. // #import <Foundation/Foundation.h> #import <sqlite3.h> @interface OperationSqliteTools : NSObject /******************************* *功能:打开数据库 *参数:databaseName -- 数据库名称 *返回:数据库对象(sqlite3对象) *******************************/ + (sqlite3 *) openDatabaseWithName: (NSString *)databaseName; /******************************* *功能:关闭数据库 *参数:database -- sqlite3 对象 *返回:空 *******************************/ + (void) closeDatabaseWithName: (sqlite3 *)database; /******************************* *功能:查询数据,无绑定变量 *参数:database -- sqlite3 对象, SQL:要执行的SQL查询语句 *返回:封装成数组的查询数据 *******************************/ + (NSArray *) queryInfoWithDataBase: (sqlite3 *) database WithSQL: (NSString *) SQL; /******************************* *功能:查询数据,有绑定变量 *参数:database -- sqlite3 对象, SQL:要执行的SQL查询语句,parameter:绑定变量的值 *返回:封装成数组的查询数据 *******************************/ + (NSArray *) queryInfoWithDataBase: (sqlite3 *) database WithSQL: (NSString *) SQL WithParameter: (NSArray *)parameter; /******************************* *功能:插入数据 *参数:database -- sqlite3 对象, SQL:要执行的SQL插入语句,parameter:绑定变量的值 *返回:插入结果,YES:插入成功, NO:插入失败 *******************************/ + (BOOL) insertDataWithDataBase: (sqlite3 *) database WithSQL: (NSString *) SQL WithParameter: (NSArray *)parameter; /******************************* *功能:更新数据 *参数:database -- sqlite3 对象, SQL:要执行的SQL插入语句,parameter:绑定变量的值 *返回:插入结果,YES:更新成功, NO:更新失败 *******************************/ + (BOOL) updateDataWithDataBase: (sqlite3 *) database WithSQL: (NSString *) SQL WithParameter: (NSArray *)parameter; /******************************* *功能:删除数据 *参数:database -- sqlite3 对象, SQL:要执行的SQL插入语句,parameter:绑定变量的值 *返回:插入结果,YES:删除成功, NO:删除失败 *******************************/ + (BOOL) deleteDataWithDataBase: (sqlite3 *) database WithSQL: (NSString *) SQL WithParameter: (NSArray *)parameter; /******************************* *功能:打印出查询后的结果 *参数:array -- 结果数组 *返回:空 *******************************/ + (void) displayResultWithArray: (NSArray *) array; @end 二、 接口的具体介绍 1、打开数据库 下面的接口是打开数据库功能,把数据库的名字传入如(Cars.sqlite),返回的是一个sqlite3的对象,你可以通过这个对象来对打开的数据库进行操作。在这个方法中,先去沙盒中查看是否有该数据库,如果有就直接打开。如果没有就从Bundle中把数据库资源复制到沙盒中,然后再从沙盒中打开。你要知道在Bundle中是无法去更改数据库中的数据的。下方是对外暴漏的接口。 /******************************* *功能:打开数据库 *参数:databaseName -- 数据库名称 *返回:数据库对象(sqlite3对象) *******************************/ + (sqlite3 *) openDatabaseWithName: (NSString *)databaseName; 该接口实现的具体方法如下,在关键代码出都加了注释,阅读代码时可以看一下注释,对于代码的东西就不做过多的赘述了。 /******************************* *功能:打开数据库 *参数:databaseName -- 数据库名称 *返回:数据库对象(sqlite3对象) *******************************/ + (sqlite3 *) openDatabaseWithName: (NSString *)databaseName{ //将数据库文件复制到沙盒中 NSFileManager *fileManager = [NSFileManager defaultManager]; //获取沙盒路径 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = paths[0]; //拼接出数据库文件在沙盒中的路径 NSString *sqlPath = [documentDirectory stringByAppendingPathComponent:databaseName]; //判断沙盒中是否已经存在我们要打开的数据库文件 BOOL success = [fileManager fileExistsAtPath:sqlPath]; //不存在的情况,会从Bundle中把资源复制过去 if (!success) { NSString *defautlDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:databaseName]; NSError *error = nil; success = [fileManager copyItemAtPath:defautlDBPath toPath:sqlPath error:&error]; if (!success) { NSLog(@"%@", [error localizedDescription]); } } //把路径转成C字符串 const char * filePath = [sqlPath UTF8String]; //声明sqlite3对象 sqlite3 * database = nil; //打开数据库 int result = sqlite3_open_v2(filePath, &database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); //成功打开 if (result == SQLITE_OK) { return database; } return nil; } 2.关闭数据库 关闭数据库就比较简单了,直接把传入的sqlite3对象进行一个关闭即可,具体代码如下: /******************************* *功能:关闭数据库 *参数:database -- sqlite3 对象 *返回:空 *******************************/ + (void) closeDatabaseWithName: (sqlite3 *)database{ sqlite3_close(database); } 3. 代码好多,博客篇幅有限,就不一一的去往上粘贴代码了,具体代码实现回在GitHub上进行分享,gitHub连接请看本博客的末尾处,在代码中也是在关键部分添加了相应的注释。 三、实例实现 调用上述简单封装的方法实现实例,对Cars.sqlite数据中其中一个表进行操作。先读取数据库中的数据,在TableView上进行加载,然后可以对数据进行添加和删除操作,更新操作就不做演示了。在插入操作中有如果有这条数据就进行Replace,这变相是一个update操作。 下方是Demo的运行效果,为了体现数据插入和删除的变化效果,给我们的Cell添加了一个动画效果,便于观察数据的变化。这个Demo也会在Github上进行分享,你可以自己运行去看一下效果。下方是动态的运行效果。为了简化操作,点击加号会有预先设定好的数据进行插入(当然你可以把用户输入的数据进行一个添加),删除的话就是TableView自带的效果删除。 下方Demo的实现并没有什么困难之处,就是对TableView的简单操作,如果你感兴趣的话,可以从Github上进行clone,然后进行扩展,添加上搜索,更新等功能。关于CoreData的操作就要看之前的博客《iOS开发之表视图爱上CoreData》.
网上关于这个主题的内容比较少,所以就写一下按装后的心得。之前一直在windows下用xampp,想在Linux下也体验一把,可是自己的Linux装的是64位的在XAMPP的官网上http://www.apachefriends.org/zh_cn/xampp.html没有Linux下64位的版本。 以下是安装步骤: 一:在官网上下载XAMPP for Linux32位 下载地址:http://www.apachefriends.org/zh_cn/xampp-linux.html 目前版本是:XAMPP Linux x86_64 1.8.2(下载后扩展名为.run) 二:在64位安装32位支持包(unbuntu命令) sudo apt-get install ia32-libs 三:装完32位支持包后把下载好的xampp-linux-1.8.2-0-installer.run移动到opt目录下 sudo mv xampp-linux-1.8.2-0-installer.run /opt/ 四:接下来就是怎么处理.run的文件的问题了,之前装一些东西大部分都是tar.gz的不罗嗦了,直入正题: 1. 第一条命令:sudo chmod +x xampp-linux-1.8.2-0-installer.run 2.第二条命令:./xampp-linux-1.8.2-0-installer.run 五:然后就会弹出XAMPP的安装界面,然后就下一步,下一步了。安装完成后在opt下会出现文件夹lampp,lampp就按好了开始你的Linux下的PHP之路吧。
学php学了有一段时间了总结总结给大家分享一下 PHP中的引用 第一段程序: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 输出结果为: firstName secondName 第二段程序: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 输出结果为:firstName 如果把原变量给释放掉则自动赋值给引用变量 php中的常量定义 define() 定义形式:define("常量名","常量值",是否大小不写敏感(默认false)); 常量名:用大写字母和下划线组成; true:大小写不敏感; false:大小写敏感 事例代码如下: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 输出结果: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 注:常量不允许重定义! ==与===号的区别 ===加上类别判断 代码如下: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 运行结果: 1=='1' 1不等于'1' web server简图 客户端Cookie Cookie是Web服务器在客户端电脑上存储的一个很小的文件。Cookie有名字(用来标识),值,也有作为可选的过期时间、路径、安全设置。 设置Cookie的代码如下: 1 $data="Cookie的内容"; 2 setcookie("CookieName",$data); 3 setcookie("AnotherCookieName",$data,time()+60);//时间的单位是秒 4 $new=$_COOKIE['CookieName'];//调用Cookie Cookie是给用户留下的垃圾,一般情况下把有效时间设置为零 服务器端session session的存储路径在php.ini中的session.save_path控制,在session开始之前必须用session_start()函数启动 exp: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 输出结果: 2013-07-07 13:07:24 $_GET超全局变量 $_GET为超链接传值 exp: 访问URL http://localhost/login.php?login='hehe'&id=3 login.php中的代码如下: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 结果为: hehe 3 好处:向指定的文件中传参数。缺点:参数的值有限大约4K左右 $_POST $_POST同$_GET一样可以从一个页面把值传到另一个页面,但是不是通过URL传递的,最常用的是表单提交比$_GET要安全一些 exp: 前端页如下: 按 Ctrl+C 复制代码 按 Ctrl+C 复制代码 后台代码: $user_name=$_POST['username']; $pwd=$_POST['pwd']; POST传值的方式内容可以更多大约2G左右 $_REQUEST超全局数组
现在有点小兴奋,因为在在BAE上部署ThinkPHP框架的问题快折腾一天了,午觉都没睡,不过没白整总算有点结果。不扯淡了,直入正题吧. 之前熟悉ThinkPHP框架,想在BAE上用ThinkPHP做点东西,部署了一天的环境了总结一下把: 一:首先你得有百度帐号吧,别着急先登上。然后进入快速创建应用如下图所示 二.创建应用的具体过程就不多说了吧不是今天的重点,然后ThinkPHP官网上去下一个云引擎版本链接如下:http://www.thinkphp.cn/down.html,我是用的ThinkPHP3.1.2的版本。截图如下: 三、在本地的服务器上把对应的文件建好,我用的是XAMPP(这个不是重点) 入口文件index.php的内容入下: <?php //1.确定应用名称Home define("APP_NAME","Home"); //2.确定应用路径 define("APP_PATH","./Home/"); //3.开启deBug模式 define("APP_DEBUG",true); //4.引入核心文件 require"./ThinkPHP/ThinkPHP.php"; ?> 在本地访问Index文件就OK了,然后压缩成.zip的文件,上传到BAE上就OK了。 四.上传到BAE后启动在线编辑模式如图所示: 找到Thinkphp下的Lib/convention.php修改数据库的配置项: 配置代码如下: 'DB_TYPE' => 'mysql', // 数据库类型 'DB_HOST' => getenv('HTTP_BAE_ENV_ADDR_SQL_IP'), // 服务器地址 'DB_NAME' => 'DKJMKVBbrCCtWpgjinXh', // 数据库名 'DB_USER' => getenv'HTTP_BAE_ENV_AK'), // 用户名 'DB_PWD' => getenv('HTTP_BAE_ENV_SK'), // 密码 'DB_PORT' => getenv('HTTP_BAE_ENV_ADDR_SQL_PORT'), // 端口 到这就部署好了,就可以开发你的web应用了。注意的是有些文集是必须在本地上生成的比如Home,admin等需要引入Thinkphp.php的文件都需要本地生成在配置数据库是也要小心不然会连接失败的哦~
在BAE上若想用ThinkPHP的那一套URL路由方式,必须在BAE上rewrite一下,之前我就直接用了不过是403错误,rewrite方式如下: 把你在BAE上的文件checkout到本地,在根目录下的app.conf(没有可以手动创建一个),然后把下面的代码粘贴上保存即可: handlers: - expire : .jpg modify 10 years - expire : .swf modify 10 years - expire : .png modify 10 years - expire : .gif modify 10 years - expire : .JPG modify 10 years - expire : .ico modify 10 years - url : (.*\.html) script : $1 - url : (.*\.css) script : $1 - url : (.*\.js) script : $1 - url : (.*\.jpg) script : $1 - url : (.*\.gif) script : $1 - url : (.*\.jpeg) script : $1 - url : (.*\.png) script : $1 - url : (.*)\?(.*) script : index.php?$2 - url : (.*) script : index.php?$1
有一段时间不写关于AJAX的东西了,最近和同学参加个比赛,要做一个类似博客的东西,用到了AJAX的东西,在写东西之前为了再熟悉一下AJAX,自己做了个关于AJAX的小事例与大家分享一下。 AJAX在js里可谓是一个牛气冲天的一个词,我刚学的时候有点望名生畏。对于初学者来说AJAX看似很难,图书馆里有些关于AJAX的教程比板砖都厚,看了就不想学。但当你真正长用的东西其实就那么写。在这就不扯那些书上扯的AJAX的历史考古的淡了,不然的话会碎的,你懂的。OK直入正题。 在这呢我主要说一下AJAX的用法,原理就不多说了。 1.你要用AJAX首先得会js吧,这个不用多说。 首先你得NEW一个AJAX的对象,类必须得事例化才能使用,这个大家都知道对吧 第一步:var oAjax = new XMLHttpRequest(); 但是为了兼容IE6这么蛋疼的浏览器一般这么写: if(window.XMLHttpRequest) { var oAjax = new XMLHttpRequest(); } else { //IE var oAjax=new ActiveXObject("Microsoft.XMLHTTP"); } 到这为止对象就事例化好了。 2.第二步咱得给服务器连接起来吧,这是必须的啊; 用open();用法是这样的:open(传输方式,文件地址,同步还是异步(默认异步)) oAjax.open('get','ajax.php?hehe='+sValue,true); 3.得发送请求吧: oAjax.send(); 4.就是接收返回值了,就不废话了,直接看代码吧: oAjax.onreadystatechange=function() { //oAjax.readyState 记录步骤 if(oAjax.readyState == 4) { if(oAjax.status == 200) { oDiv.innerHTML = oAjax.responseText; } else { alert("失败"); } } else { alert(oAjax.readyState);//记录步骤 } } 到此为止AJAX就OK了: 下面是我实验的完整事例: html代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <title>无标题文档</title> <script type="text/javascript"> window.onload=function() { var oBtn1=document.getElementById('btn1'); var oInput=document.getElementById("hehe"); var oDiv=document.getElementById("div1"); oBtn1.onclick=function() { var sValue=oInput.value; //alert(sValue); //1.创建Ajax对象 //只兼容非IE6的浏览器 if(window.XMLHttpRequest) { var oAjax=new XMLHttpRequest(); } else { //IE6 var oAjax=new ActiveXObject('Microsoft.XMLHTTP'); } //alert(oAjax); //2.连接服务器 //open(传输方式,文件地址,同步还是异步(默认异步)) oAjax.open('get','ajax.php?hehe='+sValue,true); //3.发送请求 oAjax.send(); //4.接收返回 oAjax.onreadystatechange=function() { //oAjax.readyState 记录步骤 if(oAjax.readyState == 4) { if(oAjax.status == 200) { oDiv.innerHTML = oAjax.responseText; } else { alert("失败"); } } else { alert(oAjax.readyState);//记录步骤 } } //oAjax.send(); } } </script> </head> <body> <form method="" action="ajax.php"> 呵呵:<input type="text" size=20 name="hehe" id="hehe"> <input type="button" value="提交" id="btn1"> </form> <div id="div1"> </div> </body> </html> 后台PHP代码ajax.php1 <?php 2 $hehe=$_GET['hehe']; 3 echo $hehe; 4 ?> 简单的AJAX用法事例到此为止,特为初学者而写,大牛可飘过……
今天一同学给我推荐了本书,说是刚出不久,内容还不错,是心灵鸡汤类的书,于是按捺不住就像在网上下一本,可是木有资源肿么办。只有在线看的,作为一个准码农,所以甭废话了,咱得用代码解决问题对吧…… 1.工欲善其事必先利其器 首先你得有个工具用吧,别想我之前似得抓个网页,就写了好多的$pattern去挨个匹配标签,作为伪程序员那哪行啊,对吧,咱得学着它Simple_html_dom 专门解析HTML文档的一东西,超好用的哦~。Simple_html_dom是什么东西在咱博客园上就有怎么用的博客,在这不做赘述。 2.代码详解 啥也甭说,还是代码说话给力,以下是抓去新浪的小说为例。 (1)首先得引入文件吧 include"simple_html_dom.php"; (2)咱这抓小说比较简单,深度就一层,不涉及到什么图的深搜广搜,你只要观察URL的规律即可 $url="http://vip.book.sina.com.cn/chapter/220331/2143";//用for循环加东西就是了 (3)如果for循环的次数太多会报错:超时提醒。甭担心这不是咱的错,这是配置文件的错,咱加上这句话就OK了 ini_set('max_execution_time', '100'); (4)实例化Simple_html_dom $html=new simple_html_dom(); (5)然后就是for循环生成一个个的URL然后提取内容了主要用到下面的东西 //从URL加载$html->load_file($url_temp); //查找class为mainContent的div$title=$html->find("div[class=mainContent] h1",0); //收集正文$content=$html->find("div[class=mainContent] div[class=contTxt1]",0); (6)适当的用正则控制以下格式 //换行$content=preg_replace($pattern,"\r\n",$content); //加空格$content=preg_replace($pattern1," ",$content); (7)当然少不了他了(去除html标签) $title=strip_tags($title);$content=strip_tags($content); (8)写入文件即可 (9)恭喜你,小说抓取成功。 3.主要用到的技术 (1)对php的熟练应用 (2)掌握正则表达式。 (3)主要是Simple_html_dom.php的使用 (4)还得注意php超时问题 当循环太多时会出现超时问题,建议不要修改配置文件
之前学正则表达式的目的是想从网上抓取点小说啊,文档啊,还有获取相应的视频连接然后批量下载。当时初学PHP根本不知道PHP有专门抓包的工具,就像Simple_html_dom.php(在我的其他博文中有提到),之前根本就不知道有这东西,所以就自己废着劲去学习正则表达式,然后再学习PHP中正则表达式的函数是如何使用的,然后再分析自己要抓取的DOM,最后写自己的正则表达式,正则表达式,写着还可以,不过自己刚写完的正则表达式就可能看不出他是什么意思。 有种调侃正则表达式式的说法,把正则表达式叫做火星文。当第一次用到Simple_html_dom.php这个工具包时有点相见恨晚的赶脚。不过还好,毕竟自己学了正则表达式了嘛,多学点东西还是没错的。自己写正则表达式然后再结合PHP中正则表达式函数使用,好处就是比用工具包灵活。 下面是当时学习PHP中的正则表达式所总结的内容,和大家分享一下,欢迎大家批评指正 PHP中的正则表达式函数 在PHP中有两套正则表达式函数库。一套是由PCRE(Perl Compatible Regular Expression)库提供的。PCRE库使用和Perl相同的语法规则实现了正则表达式的模式匹配,其使用以“preg_”为前缀命名的函数。另一套是由POSIX(Portable Operation System interface)扩展库提供的。POSIX扩展的正则表达式由POSIX 1003.2定义,一般使用以“ereg_”为前缀命名的函数。 两套函数库的功能相似,执行效率稍有不同。一般而言,实现相同的功能,使用PCRE库的效率略占优势。下面详细介绍其使用方法。 正则表达式的匹配 1.preg_match() 函数原型:int preg_match (string $pattern, string $content [, array $matches]) preg_match ()函数在$content字符串中搜索与$pattern给出的正则表达式相匹配的内容。如果提供了$matches,则将匹配结果放入其 中。$matches[0]将包含与整个模式匹配的文本,$matches[1]将包含第一个捕获的与括号中的模式单元所匹配的内容,以此类推。该函数只 作一次匹配,最终返回0或1的匹配结果数。 2.ereg()和eregi() ereg()是POSIX扩展库中正则表达式的匹配函数。eregi()是ereg()函数的忽略大小写的版 本。二者与preg_match的功能类似,但函数返回的是一个布尔值,表明匹配成功与否。需要说明的是,POSIX扩展库函数的第一个参数接受的是正则 表达式字符串,即不需要使用分界符。 3.preg_grep() 函数原型:array preg_grep (string $pattern, array $input) preg_grep()函数返回一个数组,其中包括了$input数组中与给定的$pattern模式相匹配的单元。对于输入数组$input中的每个元素,preg_grep()也只进行一次匹配。代码6.3给出的示例简单地说明了preg_grep()函数的使用。 进行全局正则表达式匹配 1.preg_match_all() 与preg_match()函数类似。如果使用了第三个参数,将把所有可能的匹配结果放入。本函数返回整个模 式匹配的次数(可能为0),如果出错返回False。 2.多行匹配 仅仅使用POSIX下的正则表式函数,很难进行复杂的匹配操作。例如,对整个文件(尤其是多行文本)进行匹配查找。使用ereg()对此进行操作的一个方法是分行处理。 正则表达式的替换 1.ereg_replace()和eregi_replace() 函数原型:string ereg_replace (string $pattern, string $replacement, string $string) string eregi_replace (string $pattern, string $replacement, string $string) ereg_replace()在$string中搜索模式字符串$pattern,并将所匹配结果替换 为$replacement。当$pattern中包含模式单元(或子模式)时,$replacement中形如“\1”或“$1”的位置将依次被这些子 模式所匹配的内容替换。而“\0”或“$0”是指整个的匹配字符串的内容。需要注意的是,在双引号中反斜线作为转义符使用,所以必须使用“\\0”,“ \\1”的形式。 eregi_replace()和ereg_replace()的功能一致,只是前者忽略大小写。 2.preg_replace() 函数原型:mixed preg_replace (mixed $pattern, mixed $replacement, mixed $subject [, int $limit]) preg_replace较ereg_replace的功能更加强大。其前三个参数均可以使用数组;第四个参数$limit可以设置替换的次数,默认为全部替换。 正则表达式的拆分 1.split()和spliti() 函数原型:array split (string $pattern, string $string [, int $limit]) 本函数返回一个字符串数组,每个单元为$string经正则表达式$pattern作为边界分割出的子串。如 果设定了$limit,则返回的数组最多包含$limit个单元。而其中最后一个单元包含了$string中剩余的所有部分。spliti是split的 忽略大小版本。 2.preg_split() 本函数与split函数功能一致。
编程怎么能少的了数组呢,以下是学习PHP时常用的数组处理函数。在编程中要遵循一个原则就是DRY(Don`t Repeat Yourself)原则,PHP中有大量的函数,都记住这些函数不太现实,但常用的函数还是要熟练使用的,大部分的函数的使用方法可以通过查询PHP的手册来使用。在编程中查手册是少不了的,所以要会学着使用已有的东西,就如PHP中的数组处理函数已经有排序函数了,为什么还要在写东西是费着劲去写冒泡或者堆排或者快排呢。 编程是间接的过程,也是重用的过程,要写出好的代码是少不了设计模式来做支撑的,可能对初学者来说学习设计模式有些吃力(就像我当初看设计模式时,真是有点费劲),不过等你的代码量有一定积累时,在研究设计模式时,感觉设计模式真的挺有用的,能帮助你写出漂亮的代码。说着说着有点跑偏了,还是来总结一下php中对数组操作的常用函数吧。 以下总结的数组常用的函数,可能有些读者会感觉有些少,众人拾柴火焰高吗,如果感觉还有其他常用的数组处理函数,给个评论留下呗,不要吝啬自己的知识嘛,和别人分享东西不是一件很快乐的东西吗。还有,下面的代码出自本人之手,不过是两年前写的代码了,欢迎大家批评指正。 array_splice() 删除数组中的指定元 array_splice(数组名,从前往后删的个数,new一个数组的大小);没有第三参数也就没有返数组,没有第三个参数时,第二个参数的意义为从前往后保留几个 exp: <?php $my_array=array( //建立数组 "hehe"=>"haha", "A"=>"lu", "lu"=>"ge" ); $new=array_splice($my_array,1,3); //使用array_splice(数组名,从前往后删的个数,new一个数组的大小); var_dump($new); ?> 结果:array(2) { ["A"]=> string(2) "lu" ["lu"]=> string(2) "ge" } 2、foreach()数组的遍历 用法:foreach(数组 as 键名=>键值)或foreach(数组 as 键值) exp: <?php $my_array=array( //建立数组 "hehe"=>"haha", "A"=>"lu", "lu"=>"ge" ); foreach($my_array as $key=>$value) { echo $key."=>".$value."<br/>"; } ?> 输出结果: hehe=>haha A=>lu lu=>ge 3、数组的排序 (1)sort()和rsort() 按键值排序sort()从小到大,rsort()从大到小 sort () exp : <?php $my_array=array(1,2,3,6,7,8,9,4,5);//建立数组 sort($my_array); foreach($my_array as $keys=>$value) { echo $keys."=>".$value."<br/>"; } ?> 输出结果: 0=>1 1=>2 2=>3 3=>4 4=>5 5=>6 6=>7 7=>8 8=>9 rsort() exp: <?php $my_array=array(1,2,3,6,7,8,9,4,5);//建立数组 rsort($my_array); foreach($my_array as $keys=>$value) { echo $keys."=>".$value."<br/>"; } ?> 输出结果: 0=>9 1=>8 2=>7 3=>6 4=>5 5=>4 6=>3 7=>2 8=>1 (2).asort()和arsort()和上面的原理一样,不过不改变键名和键值的对应关系 exp: <?php $my_array=array(1,2,3,6,7,8,9,4,5);//建立数组 asort($my_array); foreach($my_array as $keys=>$value) { echo $keys."=>".$value."<br/>"; } ?> 输出结果: 0=>1 1=>2 2=>3 7=>4 8=>5 3=>6 4=>7 5=>8 6=>9 (3)ksort()和krsort()是按键名的大小排序 4.数组的数学类函数 array_sum()计算数组的所有键值的和 count()计算元素的个数 exp: <?php $my_array=array(1,2,3,6,7,8,9,4,5);//建立数组 echo array_sum($my_array); ?> 输出结果:45 5.其他函数 array_unique() 去除数组中的相同元素 in_array()检测一个值是否在数组中(返回true和false) array_search()返回的是键或值,返回的是键值所对应的键名 shuffle()打乱原有的数组 <?php $my_array=array(1,2,3,6,7,8,9,4,5,5,5,5);//建立数组 array_unique($my_array);//去除数组中的相同元素 var_dump($my_array); echo "<br/>"; echo in_array(5,$my_array); echo "<br/>"; $new=array_search(6,$my_array);//返回的是键值所对应的键名 echo $new; ?> 输出结果: array(12) { [0]=> int(1) [1]=> int(2) [2]=> int(3) [3]=> int(6) [4]=> int(7) [5]=> int(8) [6]=> int(9) [7]=> int(4) [8]=> int(5) [9]=> int(5) [10]=> int(5) [11]=> int(5) }
这里我们介绍的是 40+ 个非常有用的 Oracle 查询语句,主要涵盖了日期操作,获取服务器信息,获取执行状态,计算数据库大小等等方面的查询。这些是所有 Oracle 开发者都必备的技能,所以快快收藏吧! 日期/时间 相关查询 获取当前月份的第一天 运行这个命令能快速返回当前月份的第一天。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 1 SELECT TRUNC (SYSDATE, 'MONTH') "First day of current month" 2 FROM DUAL; 获取当前月份的最后一天 这个查询语句类似于上面那个语句,而且充分照顾到了闰年,所以当二月份有 29 号,那么就会返回 29/2 。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 1 SELECT TRUNC (LAST_DAY (SYSDATE)) "Last day of current month" 2 FROM DUAL; 获取当前年份的第一天 每年的第一天都是1 月1日,这个查询语句可以使用在存储过程中,需要对当前年份第一天做一些计算的时候。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 1 SELECT TRUNC (SYSDATE, 'YEAR') "Year First Day" FROM DUAL; 获取当前年份的最后一天 类似于上面的查询语句。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 1 SELECT ADD_MONTHS (TRUNC (SYSDATE, 'YEAR'), 12) - 1 "Year Last Day" FROM DUAL 获取当前月份的天数 这个语句非常有用,可以计算出当前月份的天数。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 1 SELECT CAST (TO_CHAR (LAST_DAY (SYSDATE), 'dd') AS INT) number_of_days 2 FROM DUAL; 获取当前月份剩下的天数 下面的语句用来计算当前月份剩下的天数。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 1 SELECT SYSDATE, 2 LAST_DAY (SYSDATE) "Last", 3 LAST_DAY (SYSDATE) - SYSDATE "Days left" 4 FROM DUAL; 获取两个日期之间的天数 使用这个语句来获取两个不同日期自检的天数。 1 SELECT ROUND ( (MONTHS_BETWEEN ('01-Feb-2014', '01-Mar-2012') * 30), 0) 2 num_of_days 3 FROM DUAL; 4 5 OR 6 7 SELECT TRUNC(sysdate) - TRUNC(e.hire_date) FROM employees; 如果你需要查询一些特定日期的天数,可以使用第二个查询语句。这个例子是计算员工入职的天数。 显示当前年份截止到上个月每个月份开始和结束的日期 这个是个很聪明的查询语句,用来显示当前年份每个月的开始和结束的日期,你可以使用这个进行一些类型的计算。你可以用任何的日期值替换 “SYSDATE”来指定查询的日期。 01 SELECT ADD_MONTHS (TRUNC (SYSDATE, 'MONTH'), i) start_date, 02 TRUNC (LAST_DAY (ADD_MONTHS (SYSDATE, i))) end_date 03 FROM XMLTABLE ( 04 'for $i in 0 to xs:int(D) return $i' 05 PASSING XMLELEMENT ( 06 d, 07 FLOOR ( 08 MONTHS_BETWEEN ( 09 ADD_MONTHS (TRUNC (SYSDATE, 'YEAR') - 1, 12), 10 SYSDATE))) 11 COLUMNS i INTEGER PATH '.'); 获取直到目前为止今天过去的秒数(从 00:00 开始算) 1 SELECT (SYSDATE - TRUNC (SYSDATE)) * 24 * 60 * 60 num_of_sec_since_morning 2 FROM DUAL; 获取今天剩下的秒数(直到 23:59:59 结束) 1 SELECT (TRUNC (SYSDATE+1) - SYSDATE) * 24 * 60 * 60 num_of_sec_left 2 FROM DUAL; 数据字典查询 检查在当前数据库模式下是否存在指定的表 这是一个简单的查询语句,用来检查当前数据库是否有你想要创建的表,允许你重新运行创建表脚本,这个也可以检查当前用户是否已经创建了指定的表(根据这个查询语句在什么环境下运行来查询)。 1 SELECT table_name 2 FROM user_tables 3 WHERE table_name = 'TABLE_NAME'; 检查在当前表中是否存在指定的列 这是个简单的查询语句来检查表里是否有指定的列,在你尝试使用 ALTER TABLE 来添加新的列新到表中的时候非常有用,它会提示你是否已经存在这个列。 1 SELECT column_name AS FOUND 2 FROM user_tab_cols 3 WHERE table_name = 'TABLE_NAME' AND column_name = 'COLUMN_NAME'; 显示表结构 这 个查询语句会显示任何表的 DDL 状态信息。请注意我们已经将‘TABLE’作为第一个信息提交了。这个查询语句也可以用来获取任何数据库对象的 DDL 状态信息。举例说明,只需要把第一个参数替换成‘VIEW’,第二个修改成视图的名字,就可以查询视图的 DDL 信息了。 1 SELECT DBMS_METADATA.get_ddl ('TABLE', 'TABLE_NAME', 'USER_NAME') FROM DUAL; 获取当前模式 这是另一个可以获得当前模式的名字的查询语句。 1 SELECT SYS_CONTEXT ('userenv', 'current_schema') FROM DUAL; 修改当前模式 这是另一个可以修改当前模式的查询语句,当你希望你的脚本可以在指定的用户下运行的时候非常有用,而且这是非常安全的一个方式。 1 ALTER SESSION SET CURRENT_SCHEMA = new_schema; 数据库管理查询 数据库版本信息 返回 Oracle 数据库版本 1 SELECT * FROM v$version; 数据库默认信息 返回一些系统默认的信息 1 SELECT username, 2 profile, 3 default_tablespace, 4 temporary_tablespace 5 FROM dba_users; 数据库字符设置信息 显示数据库的字符设置信息 1 SELECT * FROM nls_database_parameters; 获取 Oracle 版本 1 SELECT VALUE 2 FROM v$system_parameter 3 WHERE name = 'compatible'; 存储区分大小写的数据,但是索引不区分大小写 某些时候你可能想在数据库中查询一些独立的数据,可能会用 UPPER(..) = UPPER(..) 来进行不区分大小写的查询,所以就想让索引不区分大小写,不占用那么多的空间,这个语句恰好能解决你的需求 。 1 CREATE TABLE tab (col1 VARCHAR2 (10)); 2 3 CREATE INDEX idx1 4 ON tab (UPPER (col1)); 5 6 ANALYZE TABLE a COMPUTE STATISTICS; 调整没有添加数据文件的表空间 另一个 DDL 查询来调整表空间大小 1 ALTER DATABASE DATAFILE '/work/oradata/STARTST/STAR02D.dbf' resize 2000M; 检查表空间的自动扩展开关 在给定的表空间中查询是否打开了自动扩展开关 1 SELECT SUBSTR (file_name, 1, 50), AUTOEXTENSIBLE FROM dba_data_files; 2 3 (OR) 4 5 SELECT tablespace_name, AUTOEXTENSIBLE FROM dba_data_files; 在表空间添加数据文件 在表空间中添加数据文件 1 ALTER TABLESPACE data01 ADD DATAFILE '/work/oradata/STARTST/data01.dbf' 2 SIZE 1000M AUTOEXTEND OFF; 增加数据文件的大小 给指定的表空间增加大小 1 ALTER DATABASE DATAFILE '/u01/app/Test_data_01.dbf' RESIZE 2G; 查询数据库的实际大小 给出以 GB 为单位的数据库的实际大小 1 SELECT SUM (bytes) / 1024 / 1024 / 1024 AS GB FROM dba_data_files; 查询数据库中数据占用的大小或者是数据库使用细节 给出在数据库中数据占据的空间大小 1 SELECT SUM (bytes) / 1024 / 1024 / 1024 AS GB FROM dba_segments; 查询模式或者用户的大小 以 MB 为单位给出用户的空间大小 1 SELECT SUM (bytes / 1024 / 1024) "size" 2 FROM dba_segments 3 WHERE owner = '&owner'; 查询数据库中每个用户最后使用的 SQL 查询 此查询语句会显示当前数据库中每个用户最后使用的 SQL 语句。 01 SELECT S.USERNAME || '(' || s.sid || ')-' || s.osuser UNAME, 02 s.program || '-' || s.terminal || '(' || s.machine || ')' PROG, 03 s.sid || '/' || s.serial# sid, 04 s.status "Status", 05 p.spid, 06 sql_text sqltext 07 FROM v$sqltext_with_newlines t, V$SESSION s, v$process p 08 WHERE t.address = s.sql_address 09 AND p.addr = s.paddr(+) 10 AND t.hash_value = s.sql_hash_value 11 ORDER BY s.sid, t.piece; 性能相关查询 查询用户 CPU 的使用率 这个语句是用来显示每个用户的 CPU 使用率,有助于用户理解数据库负载情况 1 SELECT ss.username, se.SID, VALUE / 100 cpu_usage_seconds 2 FROM v$session ss, v$sesstat se, v$statname sn 3 WHERE se.STATISTIC# = sn.STATISTIC# 4 AND NAME LIKE '%CPU used by this session%' 5 AND se.SID = ss.SID 6 AND ss.status = 'ACTIVE' 7 AND ss.username IS NOT NULL 8 ORDER BY VALUE DESC; 查询数据库长查询进展情况 显示运行中的长查询的进展情况 01 SELECT a.sid, 02 a.serial#, 03 b.username, 04 opname OPERATION, 05 target OBJECT, 06 TRUNC (elapsed_seconds, 5) "ET (s)", 07 TO_CHAR (start_time, 'HH24:MI:SS') start_time, 08 ROUND ( (sofar / totalwork) * 100, 2) "COMPLETE (%)" 09 FROM v$session_longops a, v$session b 10 WHERE a.sid = b.sid 11 AND b.username NOT IN ('SYS', 'SYSTEM') 12 AND totalwork > 0 13 ORDER BY elapsed_seconds; 获取当前会话 ID,进程 ID,客户端 ID 等 这个专门提供给想使用进程 ID 和 会话 ID 做些 voodoo magic 的用户。 1 SELECT b.sid, 2 b.serial#, 3 a.spid processid, 4 b.process clientpid 5 FROM v$process a, v$session b 6 WHERE a.addr = b.paddr AND b.audsid = USERENV ('sessionid'); V$SESSION.SID AND V$SESSION.SERIAL# 是数据库进程 ID V$PROCESS.SPID 是数据库服务器后台进程 ID V$SESSION.PROCESS 是客户端 PROCESS ID, ON windows it IS : separated THE FIRST # IS THE PROCESS ID ON THE client AND 2nd one IS THE THREAD id. 查询特定的模式或者表中执行的最后一个 SQL 语句 1 SELECT CREATED, TIMESTAMP, last_ddl_time 2 FROM all_objects 3 WHERE OWNER = 'MYSCHEMA' 4 AND OBJECT_TYPE = 'TABLE' 5 AND OBJECT_NAME = 'EMPLOYEE_TABLE'; 查询每个执行读取的前十个 SQL 01 SELECT * 02 FROM ( SELECT ROWNUM, 03 SUBSTR (a.sql_text, 1, 200) sql_text, 04 TRUNC ( 05 a.disk_reads / DECODE (a.executions, 0, 1, a.executions)) 06 reads_per_execution, 07 a.buffer_gets, 08 a.disk_reads, 09 a.executions, 10 a.sorts, 11 a.address 12 FROM v$sqlarea a 13 ORDER BY 3 DESC) 14 WHERE ROWNUM < 10; 在视图中查询并显示实际的 Oracle 连接 1 SELECT osuser, 2 username, 3 machine, 4 program 5 FROM v$session 6 ORDER BY osuser; 查询并显示通过打开连接程序打开连接的组 1 SELECT program application, COUNT (program) Numero_Sesiones 2 FROM v$session 3 GROUP BY program 4 ORDER BY Numero_Sesiones DESC; 查询并显示连接 Oracle 的用户和用户的会话数量 1 SELECT username Usuario_Oracle, COUNT (username) Numero_Sesiones 2 FROM v$session 3 GROUP BY username 4 ORDER BY Numero_Sesiones DESC; 获取拥有者的对象数量 1 SELECT owner, COUNT (owner) number_of_objects 2 FROM dba_objects 3 GROUP BY owner 4 ORDER BY number_of_objects DESC; 实用/数学 相关的查询 把数值转换成文字 更多信息可以查看: Converting number into words in Oracle 1 SELECT TO_CHAR (TO_DATE (1526, 'j'), 'jsp') FROM DUAL; 输出: 1 one thousand five hundred twenty-six 在包的源代码中查询字符串 这个查询语句会在所有包的源代码上搜索‘FOO_SOMETHING’ ,可以帮助用户在源代码中查找特定的存储过程或者是函数调用。 1 --search a string foo_something in package source code 2 SELECT * 3 FROM dba_source 4 WHERE UPPER (text) LIKE '%FOO_SOMETHING%' 5 AND owner = 'USER_NAME'; 把用逗号分隔的数据插入的表中 当 你想把用逗号分隔开的字符串插入表中的时候,你可以使用其他的查询语句,比如 IN 或者是 NOT IN 。这里我们把‘AA,BB,CC,DD,EE,FF’转换成包含 AA,BB,CC 等作为一行的表,这样你就很容易把这些字符串插入到其他表中,并快速的做一些相关的操作。 1 WITH csv 2 AS (SELECT 'AA,BB,CC,DD,EE,FF' 3 AS csvdata 4 FROM DUAL) 5 SELECT REGEXP_SUBSTR (csv.csvdata, '[^,]+', 1, LEVEL) pivot_char 6 FROM DUAL, csv 7 CONNECT BY REGEXP_SUBSTR (csv.csvdata,'[^,]+', 1, LEVEL) IS NOT NULL; 查询表中的最后一个记录 这个查询语句很直接,表中没有主键,或者是用户不确定记录最大主键是否是最新的那个记录时,就可以使用这个语句来查询表中最后一个记录。 01 SELECT * 02 FROM employees 03 WHERE ROWID IN (SELECT MAX (ROWID) FROM employees); 04 05 (OR) 06 07 SELECT * FROM employees 08 MINUS 09 SELECT * 10 FROM employees 11 WHERE ROWNUM < (SELECT COUNT (*) FROM employees); 在 Oracle 中做行数据乘法 这个查询语句使用一些复杂的数学函数来做每个行的数值乘法。更多内容请查阅: Row Data Multiplication In Oracle 01 WITH tbl 02 AS (SELECT -2 num FROM DUAL 03 UNION 04 SELECT -3 num FROM DUAL 05 UNION 06 SELECT -4 num FROM DUAL), 07 sign_val 08 AS (SELECT CASE MOD (COUNT (*), 2) WHEN 0 THEN 1 ELSE -1 END val 09 FROM tbl 10 WHERE num < 0) 11 SELECT EXP (SUM (LN (ABS (num)))) * val 12 FROM tbl, sign_val 13 GROUP BY val; 在 Oracle 生成随机数据 每个开发者都想能轻松生成一堆随机数据来测试数据库多好,下面这条查询语句就可以满足你,它可以在 Oracle 中生成随机的数据插入到表中。详细信息可以查看 Random Data in Oracle 01 SELECT LEVEL empl_id, 02 MOD (ROWNUM, 50000) dept_id, 03 TRUNC (DBMS_RANDOM.VALUE (1000, 500000), 2) salary, 04 DECODE (ROUND (DBMS_RANDOM.VALUE (1, 2)), 1, 'M', 2, 'F') gender, 05 TO_DATE ( 06 ROUND (DBMS_RANDOM.VALUE (1, 28)) 07 || '-' 08 || ROUND (DBMS_RANDOM.VALUE (1, 12)) 09 || '-' 10 || ROUND (DBMS_RANDOM.VALUE (1900, 2010)), 11 'DD-MM-YYYY') 12 dob, 13 DBMS_RANDOM.STRING ('x', DBMS_RANDOM.VALUE (20, 50)) address 14 FROM DUAL 15 CONNECT BY LEVEL < 10000; 在 Oracle 中生成随机数值 这是 Oracle 普通的旧的随机数值生成器。这个可以生成 0-100 之间的随机数值,如果你想自己设置数值范围,那么改变乘数就可以了。 1 --generate random number between 0 and 100 2 SELECT ROUND (DBMS_RANDOM.VALUE () * 100) + 1 AS random_num FROM DUAL; 检查表中是否含有任何的数据 这个可以有很多中写法,你可以使用 count(*) 来查看表里的行的数量,但是这个查询语句比较高效和快速,而且我们只是想知道表里是否有任何的数据。 1 SELECT 1 2 FROM TABLE_NAME 3 WHERE ROWNUM = 1;
以下这些东西是我的麦库上存的当时学Oracle的学习笔记今天拿出来和大家分享一下,转载请注明出处,下面用的Oracle的版本是10g,用的时WinServer2003的操作系统,可能有些命令和Oracle11g的有所不同,但大部分是一样的,接下来还会陆续的分享一下Oracle中对用户的管理,对表的管理,还有Oracle中的存储过程和PL/SQL编程。用到的Oracle的管理工具是PL/SQL Developerl和SQL PLUS,欢迎大家批评指正。 1.数据库管理员的职责: 1.安装和升级Oracle数据库 2.创建数据库,表空间,表,视图,索引 3.制定并实施备份和恢复数据库计划 4.数据库权限管理,调优,故障排除 5.对于高级的dba可以参与项目的开发,会编写sql语句,存储过程,触发器,规则,约束,包2.管理数据库的主要用户主要是sys和system: 例如:sys是董事长,system是总经理 1.两者尊重要的区别:存储数据的重要性不同。 sys:所有oracle数据字典的基表和视图都存放在sys用户中,这些基表和视图对oracle的运行是至关重要的 由数据库自己维护,任何用户都不可手动更改,sys拥有dba,sysdba,sysoper角色或权限是oracle权限的最高用户。 system:用于存放次一级的内部数据,system拥有dba,sysoper角色或权限 2.第二个区别 sys:必须以sysdba 或 sysoper的角色登录,不能以normal方式登录3.对初始化参数的修改: 1.显示初始化参数 show parameter 2.如何修改参数 可以到文件oracle\admin\orcl\pfile\init.ora 下修改; 4.数据库的逻辑备份: 逻辑备份是指使用工具export将数据对象的结构和数据导出到文件的过程。 逻辑回复是指当数据库被误操作后利用工具import利用备份的数据导入到数据库的过程。 物理备份即可在数据库open下操作也可以在关闭下操作。 但逻辑备份只能在数据库的open下操作。 数据的导出:!!!--在导入导出的时候要到oracle目录的bin目录下执行命令; 导出具体的分为:导出表,导出方案,导出数据库 导出用exp命令来实现,该命令常用的选项有: userid:用于指定执行导出操作的用户名,口令,连接字符串; tables:用于指定导出操作的表; owner: 用于指定导出操作的方案; full=y:用于指定导出的数据库; inctype:用于指定导出操作的增量类型; rows:执行导出操作是否导出表中的数据; file:用于指定导出文件名; 1).导出表:导出scott用户下数据库实例orcl中emp的表,存放的文件路径为e:/emp.dmp; 在cmd的控制台下执行: C:\Documents and Settings\Administrator.WEB-A93B1E61669>exp userid=scott/tiger@orcl tables=(emp,dept) file=e:\emp.dmp; 2)导出其他方案的表 如果要导出其他方案的表则需要dba权限或者exp_full_database权限SQL>userid=system/system@orcl tables=(scott.emp) file=e:\emp1.dmp; 3)导出表的结构没有其数据exp userid=scott/tiger@orcl tables=(emp,dept) file=e:\emp.dmp rows=n; 4)直接导出表结构(适合表中有大量的数据,速度比 rows=n 要快的多)exp userid=scott/tiger@orcl tables=(emp.dept) file=e:\emp2.dmp direct=y; 2.导出方案: 1)导出scott的方案: exp scott/tiger@orcl owner=scott file=e:\scott.dmp; C:\Documents and Settings\Administrator.WEB-A93B1E61669>exp userid=scott/tiger@orcl owner=scott file=e:\scott.dmp 2).导出别的方案:如果要导出其他方案的表则需要dba权限或者exp_full_database权限exp userid=system/system@orcl owner=(system,scott) file=e:\syscott.dmp; 导出数据库 inctype=complete;增量备份--第二次备份时只会添加新增的数据库 exp userid=system/system@orcl full=y inctype=complete file=e:\orcl.dmp; 5.数据库的导入:import 将文件中的数据导入到数据库中,但导入的数据文件必须是用工具export导出的文件 导入也分导入表,导入方案,导入数据库三种方式 imp常用的命令有: userid:用于指定导入操作的用户名,口令,连接字符串; tables:用于执行导入表操作 fromuser:用于指定源用户 touser:用于指定目标用户 file:用于指定导入文件名 full=y:用于指定导入整个文件 inctype:用于指定导入文件的增量类型 rows:指定是否要导入表行(数据) ignore:如果表存在则只导入表数据 --删除表是同时会删除之前创建的savepoint,所以删除表是无法rollback 1.导入该用户下的表:imp userid=scott/tiger@orcl tables=(emp) file=e:\orcl.dmp; 2.导入表到其他用户要求DBA权限或者imp_full_database的权限;imp userid=system/system@orcl tables=(emp) file=e:\orcl.dmp touser=scott; 3.只导入表结构而不导入数据 imp userid=scott/tiger@orcl tables=(emp) file=e:\orcl.dmp rows=n; 4.如果表已存在则只导入数据 imp userid=scott/tiger@orcl tables=(emp) file=e:\orcl.dmp ignore=y; 5.导入自身方案 imp userid=scott/tiger@orcl file=e:\scott.dmp; 6.导入其他方案imp userid=system/system@orcl file=e:\scott.dmp fromuser=system touser=scott; 7.导入数据库 imp userid=system/system@orcl full=y file=e:\orcl.dmp; 6.数据字典: 数据字典记录了数据库的系统信息,它是只读表和视图的集合,数据字典的所有者是sys用户 用户只能在表上执行查询操作(select操作),而其维护和修改是系统自动完成的 数据字典包括数据基表和数据视图,数据基表里存储的是数据库的基本信息,普通用户不能直接访问数据库基表的信息,数据字典里的视图是基于 基本表创建起来的虚表,用户可以通过查询视图里的信息来查询系统的信息 数据字典里的视图主要包括user_XXX,all_XXX和dba_XXX三种类型; 1.user_tables显示当前用户所拥有的表sql>select table_name from user_tables; 2.all_tables 显示当前用户可以访问到的所有表,不仅是该方案下的,也显示其他方案下的sql>select table_name from all_tables; 3.dba_tables 显示所有方案所拥有的表,要求用户必须有dba角色或者有select_any_tables的权限如system用户。 sql>select owner,table_name from dba_tables; 4.通过查询dba_users来显示数据库中所有的用户(视图) sql>select username from dba_users; 5.通过查询dba_sys_privs视图可以查询用户所具有的系统权限 SQL>select * from dba_sys_privs where grantee='SYS' 6.通过查询dba_tab_privs视图可以查询用户所具有的对象权限 SQL> select * from dba_tab_privs where grantee='SCOTT'; 7.通过查询dba_col-Privs视图可以查询用户所具有的列权限 8.通过查询dba_role_privs 视图可以查询用户的角色;SQL> select * from dba_role_privs where grantee='SCOTT'; GRANTEE GRANTED_ROLE ADMIN_OPTION DEFAULT_ROLE ------------------------------ ------------------------------ ------------ ------------ SCOTT RESOURCE NO YES 9.查询Oracle中的所有角色 SQL> select * from dba_roles; 10.查询系统权限 SQL> select * from system_privilege_map; 11.查询对象权限 SQL> select distinct privilege from dba_tab_privs; 12.查询角色拥有的系统权限SQL>select * from dba_sys_privs where grantee='CONNECT'; 13.查询角色拥有的对象权限SQL>select * from dba_tab_privs where grantee='CONNECT';
前一段时间学习Oracle 时做的学习笔记,整理了一下,下面是分享的Oracle常用函数的部分笔记,以后还会分享其他部分的笔记,请大家批评指正。 1.Oracle 数据库中的to_date()函数的使用: 往emp表中插入一条记录: SQL> insert into emp values(1234,'LIZELU','BOSS',1234,'1980-12-06',10000.0,0,30); insert into emp values(1234,'LIZELU','BOSS',1234,'1980-12-06',10000.0,0,30) ORA-01861: 文字与格式字符串不匹配--日期格式不对 使用to_date()函数搞定:格式to_date('1965-02-05','yyyy-mm-dd'); 2.Oracle中的字符函数: 字符函数是Oracle中最常用的函数, lower(char); 把字符串转换为小写格式; upper(char);把字符串转换为大写格式; length(char);返回字符串的长度; substr(char,m,n);取字符串的字串; replace(char,search_char,replace_str); 1.将所有员工的名字按小写的格式输出 select lower(emp.ename) from emp; 2.显示正好为5个字符的名字;select ename from emp where length(ename)=5; 3.显示姓名的前三个字符;substr(char,2,3);代表从第二个取,取三个字符;select substr(ename,1,3) from emp; 4.显示姓名要求首字母大写,其余的小写; 分成三部走: (1)把首字母大写: select upper(substr(emp.ename,1,1)) from emp; (2)把后面的字母小写:select lower(substr(ename,2,length(ename)-1)) from emp; (3)把两个字符串连接起来 ||(管道符是连接作用的)select upper(substr(emp.ename,1,1))||lower(substr(ename,2,length(ename)-1)) from emp; 5.把名字中的A转换为a; select replace(ename,'A','a') from emp; 3.Oracle 中的数学函数: 1.round(n,[m]):四舍五入,省略m则四舍五入到整数位,m为小数点的位数;select round(sal,1) from emp where ename='MILLER'; 2.trunc(n,[m]):保留小数位,m为小数位的个数select trunc(sal,1) from emp where ename='MILLER'; 3.mod(n,m):去小数; 4.floor(n):返回小于等于n的最大整数; ceil(n):返回大于等于n的最小整数SQL> select floor(sal) from emp where ename='MILLER';--向下取整 FLOOR(SAL) ---------- SQL> select ceil(sal) from emp where ename='MILLER';--向上取整 CEIL(SAL) ---------- 其他数学函数: abs(n):返回数字n的绝对值。 acos(n),asin(n),stan(n) 返回数字的反余弦,反正弦,反正切的值 exp(n):返回e的n次幂; log(m,n);返回对数值; power(m,n);返回m的n次幂 4.Oracle中的日期函数: 日期函数用于处理date类型的数据:默认情况下是dd-mon-yy格式。 (1)sysdate:该函数返回系统时间 SQL> select sysdate from dual; SYSDATE ----------- 2014-4-13 9 (2)add_moths(d,n); 显示入职8个多月的职工; select * from emp where sysdate>add_months(emp.hiredate,8); (3)last_day(d);返回当前日期该月的最后一天 select last_day(emp.hiredate) from emp; (4)显示员入职的天数SQL> select ename,round(sysdate-emp.hiredate) "入职天数" from emp; (5) 找出个月的倒数第3天入职的员工 SQL> select * from emp where (last_day(emp.hiredate)-emp.hiredate)=2; 5.Oracle中数据类型的转换 to_char():把数据转换为字符串类型:to_char(字符串,类型); 1.日期转换 SQL> select to_char(sysdate,'yyyy/mm/dd hh24:mi:ss') from dual; TO_CHAR(SYSDATE,'YYYY/MM/DDHH2 ------------------------------ 2014/04/13 10:13:52 2.显示1980年入职的员工信息SQL> select * from emp where to_char(emp.hiredate,'yyyy')=1980; EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ----- ---------- --------- ----- ----------- --------- --------- ------ LIZELU BOSS 1234 1980-12-6 10000.00 0.00 30 SMITH CLERK 7902 1980-12-17 800.00 20 6.Oracle中的系统函数:sys_context(); 1) terminal 当前会话客户所对应的终端标识符 SQL> select sys_context('USERENV','terminal') from dual; SYS_CONTEXT('USERENV','TERMINA -------------------------------------------------------------------------------- WEB-A93B1E61669 2) language 语言SQL> select sys_context('USERENV','language') from dual; SYS_CONTEXT('USERENV','LANGUAG -------------------------------------------------------------------------------- SIMPLIFIED CHINESE_CHINA.ZHS16GBK 3)db_name 当前的数据库实例名称 SQL> select sys_context('USERENV','db_name') from dual; SYS_CONTEXT('USERENV','DB_NAME -------------------------------------------------------------------------------- orcl 4)session_user 当前会话所对应的数据库 SQL> select sys_context('USERENV','session_user') from dual; SYS_CONTEXT('USERENV','SESSION -------------------------------------------------------------------------------- SCOTT 5)current_schema:查看当前方案SQL> select sys_context('USERENV','current_schema') from dual; SYS_CONTEXT('USERENV','CURRENT -------------------------------------------------------------------------------- SCOTT
自己在学习Oracle是做的笔记及实验代码记录,内容挺全的,也挺详细,发篇博文分享给需要的朋友,共有1w多字的学习笔记吧。是以前做的,一直在压箱底,今天拿出来整理了一下,给大家分享,有不足之处还望大家批评指正。 PL/SQL定义:PL/SQL是由Oracle开发,专门用于Oracle的程序设计语言。 PL---Procedural Language. SQL—Structure QueryLanguage。PL/SQL包括过程化语句和SQL语句 PL/SQL的单位:块。 一个块中可以嵌套子块。 块的三个组成部分: 一:定义部分(declare) PL/SQL中使用的变量,常量,游标和异常的名字都必须先定义后使用。并且定义在以declare关键字开头的定义部分 二:可执行部分:(begin) 是PL/SQL的主题,包含该块的可执行语句,该部分定义了块的功能,是必须的部分。由关键字begin开始,end结束 三:异常处理部分:(exception) 该部分以exception开始,以end结束 Demo: DECLARE –可选 变量,常量,游标,用户自定义的特殊类型 BEGIN –必须 --SQL语句 --PL/SQL语句 EXCEPTION –可选 --异常处理部分 END; --必须 即由声明,执行,异常组成 DEMO: DECLARE V_value1 VARCHAE2(5); BEGIN SELECT cn_name INTO v_value1 FROM table_name; EXCEPTION WHEN exception_name THEN --处理程序…… END; PL/SQL的优点: 1、 改善了性能:PL/SQL把整个语句块发送给服务器,这个过程在单次调用中完成,降低了网络拥挤 2、 可重用性:只要有Oracle的地方都能运行 3、 模块化:程序中的每一块都实现一个逻辑操作,有效的进行了分割。 PL/SQL块的类: 1、 匿名块:只能存储一次,不能存储在数据库中 2、 过程,函数和包(procedure,function,package):是命了名的PL/SQL块,被存储在数据库中,可以被多次使用,可以用外部程序显示执行。 3、 触发器:是命名的PL/SQL块,被存储在数据库中,当触发某事件时自动执行。 PL/SQL中变量的命名规范: 1、 至多有30个字符 2、 不能是保留字 3、 必须以字母开头 4、 不允许和数据库中表的列名相同 5、 不可包括$,_和数字以外的字符 PL/SQL中的变量 1、 PL/SQL变量 a) 标量型:只能存放单一值 b) 复合型 c) 引用型 d) LOBx型:存放大数据 2、 定义变量语法 a) 变量名 变量类型 := 变量值 b) V_number NUMBER(2) NOT NULL :=20; c) 常量的定义 i. V_number CONSTANT NUMBER(2,3) :=20.098; DEMO:查询员工号为7369的员工,把其job存入v_job中并输出 DECLARE --定义存储job的变量v_job为引用变量与--emp.job的类型相同,用%TYPE实现 v_job emp.job%TYPE; --定义员工号变量并赋初值(:=) n_empno emp.empno%TYPE:=7369; BEGIN --查询语句 SELECT emp.job --把查出来的结果赋给v_job INTO v_job FROM emp WHERE emp.empno = n_empno; --打印输出结果 Dbms_Output.put_line(v_job); END; %TYPE属性: 通过%TYPE声明一个变量,实际上就是参照变量或表中的字段的类型作为变量的类型,并且保持同步。 变量将遵循下面的类型声明: 1. 已声明过的变量类型 2. 数据库中表字段的类型 demo1: 创建一个匿名块,输出hello world --创建一个匿名块,输出hello world DECLARE v_hello varchar2(20) :='Hello World'; BEGIN Dbms_Output.put_line(v_hello); END; --创建一个匿名块,查询emp表,显示雇员名是’SCOTT‘的薪水,通过DBMS_OUTPUT包来显示。 DECLARE v_sal emp.sal%TYPE; v_name emp.ename%TYPE := 'SCOTT'; BEGIN SELECT emp.sal INTO v_sal FROM emp WHERE emp.ename = v_name; dbms_output.put_line(v_sal); END; demo2: -从部门表中找到最大的部门号,将其输出到屏幕--从部门表中找到最大的部门号,将其输出到屏幕 DECLARE v_deptno dept.deptno%TYPE; BEGIN SELECT MAX(dept.deptno) INTO v_deptno FROM dept; dbms_output.put_line(v_deptno); END; demo3: --PL/SQL嵌套和变量的作用域 --PL/SQL嵌套和变量的作用域 DECLARE v_parent NUMBER :=10; BEGIN DECLARE v_child NUMBER :=20; BEGIN dbms_output.put_line('chile='||v_child); dbms_output.put_line('parent='||v_parent); END; --dbms_output.put_line('chile='||v_child); --注意变量的作用域 dbms_output.put_line('chile='||v_parent); END; 结果: chile=20 parent=10 chile=10 demo4: --选择并打印emp表中薪水总和--选择并打印emp表中薪水总和 DECLARE v_sal emp.sal%TYPE; BEGIN SELECT sum(emp.sal) INTO v_sal FROM emp; dbms_output.put_line(v_sal); END demo5:事务的操作 DECLARE v_sal emp.sal%TYPE :=800; BEGIN UPDATE emp SET emp.sal = emp.sal+ v_sal WHERE emp.job='ANALYST'; SAVEPOINT a; UPDATE emp SET emp.sal = emp.sal+ v_sal WHERE emp.job='ANALYST'; SAVEPOINT b; ROLLBACK TO SAVEPOINT a; COMMIT; END; 编写控制结构 1、 条件分支语句 a) IF语句: i. – IF – THEN – END IF ii. – IF – THEN – ELSE – END IF iii. – IF – THEN – ELSEIF – END IF 2、 条件语句语法 a) IF condition THEN i. Statement; b) [ELSIF condition THEN i. Statement;] c) [ELSE i. Statement;] d) ENDIF; 3、 DEMO: a) IF v_name = ‘SCOTT’ AND SAL >= 3000 THEN i. v_dept :=20; b) END IF; DEMO: --null的处理 DECLARE v_x NUMBER :=NULL; v_y NUMBER := NULL; BEGIN IF v_x = v_y THEN dbms_output.put_line('NULL等于NULL'); ELSE dbms_output.put_line('NULL不等于NULL'); END IF; END; 结果:NULL不等于NULL 空是未知的东西 4.Case语句:语法(有返回值的) CASE demo: DECLARE v_sal emp.sal%TYPE; v_dept emp.deptno%TYPE; v_result VARCHAR(20); BEGIN SELECT emp.deptno INTO v_dept FROM emp WHERE emp.sal = ( SELECT MAX(emp.sal) FROM emp ); dbms_output.put_line(v_dept); v_result := CASE v_dept WHEN 10 THEN '部门一' WHEN 20 THEN '部门二' ELSE '部门三' END; dbms_output.put_line(v_result); END; 输出结果: 10 部门一 5、 循环语句LOOP : DEMO:循环插入11条数据 DECLARE v_count NUMBER := 0; BEGIN LOOP --插入数据 INSERT INTO test(name,id,password) VALUES ('TEST'||v_count,v_count,'admin'); --变量加一 v_count := v_count+1; --判断退出条件 EXIT WHEN v_count > 10; END LOOP; END; b) FOR LOOP循环 DEMO: DECLARE v_counter NUMBER :=0; BEGIN --v_counter是自增的 FOR v_counter IN 0 .. 10 LOOP DELETE FROM test WHERE test.id = v_counter; END LOOP; END; b) WHILE LOOP DECLARE v_count NUMBER := 0; BEGIN WHILE v_count<10 LOOP --插入数据 INSERT INTO test(name,id,password) VALUES ('TEST'||v_count,v_count,'admin'); --变量加一 v_count := v_count+1; END LOOP; END; 三:复合类型 1、 复合数据类型 a) 一个复合变量可以存放多个值 b) 复合变量创建后可以多次使用 c) 如同枚举类型和数组 2、 PL/SQL记录 a) 每个记录内都有很多的不同类型的字段 b) 无初始值的字段为NULL c) Record 类型声明用户自定义的类型 3、 定义一个记录 a) 语法: 1 i. TYPE type_name IS RECORD( 2 ii. 字段名1 字段类型1, 3 iii. 字段名2 字段类型2 4 iv. ); b) DEMO i. TYPE emp_record_name IS RECORD( ii. V_name varchar(20), iii. V_password varchar(10) iv. ); v. Emp_record emp_record_name; --记录的定义与使用 DECLARE TYPE test_record_name IS RECORD( v_name test.name%TYPE, v_id test.id%TYPE, v_password test.password%TYPE ); test_record test_record_name; BEGIN SELECT test.name,test.id,test.password INTO test_record FROM test WHERE test.name='TEST0'; dbms_output.put_line(test_record.v_name||test_record.v_id||test_record.v_password); END; 5 记录的另一种定义:表名%ROWTYPE a) Exp_row table_name%ROWTYPE DEMO: --记录的定义与使用 DECLARE emp_record emp%ROWTYPE; BEGIN SELECT * INTO emp_record FROM emp WHERE emp.empno='7788'; dbms_output.put_line(emp_record.empno||' '||emp_record.sal); END; 编写游标 1、 游标的定义:游标是Oracle在数据库中开辟的一个工作区,用来存放SELECT语句查询的结果。 2、 游标的分类 a) 隐式游标:PL/SQL隐式建立并管理这一游标。 b) 显示游标:由程序员定义并控制,从数据库中读出多行数据,并从多行数据中一行一行的处理。 3、 游标的声明: a) 语法:CURSOR cursor_name IS select_statement; b) 在游标声明中SELECT语句不能使用INTO语句,可以在字句子中使用ORDER字句。 c) Demo: 1 CURSOR emp_cursor IS 2 SELECT * 3 FROM emp; 4、 打开游标 a) 语法:OPEN cursor_name; b) 使用游标之前应打开游标 c) 打开游标实际上是执行定义游标时的SELECT语句,将查询结果检索到工作区中。 d) 如果没有要返回的行没有异常 5、 从游标中提取数值 a) 语法 i. FETCH cursor_name INTO [v1,v2……]|record_name] b) 在使用FETCH时先把游标打开,不然没法使用。 c) 对游标第一次使用FETCH时,游标指向的是第一条记录,使用后游标指向下一条记录。 d) 游标只能向下移动不能回退,如果想回退到上一条记录,只有把游标关闭后在打开。 e) INTO字句中的变量个数、顺序、数据类型必须和工作区中的保持一致; 6、 关闭游标 a) 语法:CLOSE cursor_name b) 处理完数据后必须关闭游标,如果需要可以再次打开游标,游标一旦关闭不可再从游标中提取数据,当关闭游标后所有和游标相关的资源都会被关闭。 7.游标的使用Demo --游标的使用 DECLARE --定义临时变量来存放游标中的内容 emp_empno emp.empno%TYPE; emp_ename emp.ename%TYPE; --定义名为emp_cursor的游标 CURSOR emp_cursor IS SELECT emp.empno,emp.ename FROM emp; BEGIN --打开游标 OPEN emp_cursor; --循环输出游标 FOR i IN 1..5 LOOP --提取游标中的内容 FETCH emp_cursor INTO emp_empno,emp_ename; dbms_output.put_line(emp_empno||' '||emp_ename); END LOOP; --关闭游标 CLOSE emp_cursor; END;
接触面向对象也有一段时间了,当时是通过C++学习的OOP,后来又接触到了PHP和Java。每种OOP的语言在面向对象上或多或少都会有不同的地方,现在在学习OC的面向对象部分,又感觉到OC面向对象的特点。写篇博文总结一下OC中的面向对象。刚接触OC,用OC中的便利初始化方法和便利构造器有点蹩脚,不过还可以在接受的范围之内,以下的东西可能会对面向对象的特征:抽象,封装,继承等总结的较少一些,主要总结了OC中面向对象的特点。在用到便利构造器的时候,如果之前学习过设计模式的话会好理解一些。 在下面的代码实例当中,可能在便利初始化方法和便利构造器这一块理解起来有些问题。简单的说来,便利构造器是为了简化对象的初始化而生的,在之前的博客中也说了一嘴:编程是间接的过程,其实使用便利构造器就是间接的过程。在程序中处处都用到了间接。比如你定义的变量,你定义的函数,都是在间接的使用一些东西。在现实生活中间接的作用的很大的,就连找个女朋友也要间接一下,如果间接好了你很有可能和奥巴马成为好哥们不是吗,不是有种理论叫做六度人脉吗?程序中的间接的好处是什么呢? 根据个人的理解,间接原则会让自己写的代码更为灵活,会避免一些不必要的重复编写代码。函数就是一个最好的例子,把程序中不变且常用的部分进行封装,然后把变的部分用函数的参数列表传进来,这样就很好的实现代码的重用功能,这也是函数存在编程中的意义所在。伪文艺一下,哲学上不是有句话叫做世界上每种存在的事物都有他存在的意义。达尔文的适者生存论放在计算机技术发展中也是挺适用的,能被保留的东西一定会有他的作用。 言归正传,便利构造器就是对便利初始化函数的间接使用,目的是为了简化对象的初始化(这里是我个人的理解)。便利初始化函数(对象方法)的作用是给实例常量赋初值,在类的实例化后就可以调用便利初始化函数了。而便利构造器是类方法,返回的是对象,在便利构造器中做了两件事:一个是给对象分配空间,第二个是调用便利初始化函数进行数据的初始化。学过设计模式的小伙伴都应该知道“模板方法模式”,我个人感觉便利构造器和模板方法模式的作用挺相似的。 以下的东西是依附于代码来讲解的,编程吗,即使讲思想也少不了代码不是吗,话不多说,切入正题。请大家批评指正,若要转载请注明出处。 面向对象程序开发的主要目标:用代码模拟现实中的对象,将现实中对象的某些行为能力,特征用代码表现出来,然后用这些代码来模拟现实中的问题。 每个对象都会从两个角度进行描述,一个是特征,一个是行为能力 特征:可以是物体的组成部分,也可以是一些物理或逻辑上的属性,用来表现对象的形态,构成及状态。 行为能力:对象所能被进行的操作或者物体本身发起的操作。用来接受外部操作或对外部进行操作。 封装:将属性及方法相结合,共同体现对象的特征,称之为封装,封装可以实现隐藏内部实现,稳定外部接口。 在OC中类是由接口(interface)和实现(implementation)两部分构成的。在OC中类是通过两个单独的文件定义。接口定义在对应的头文件中,该文件的作用是说明此类具有哪些属性和方法,但不去实现其行为。 1. OC中接口的定义如下: #import <Foundation/Foundation.h> @interface Student : NSObject {//大括号里定义属性 //定义学生的学号,姓名,年龄,爱好; @public NSString *studentName; int age; NSString *hobby; } /*定义学生有关的方法,+修饰的为类方法,不用实例化就可以直接用类访问 * -号修饰的为对象方法,必须实例化后才能使用,就是用对象调用的方法 */ //定义打招呼的方法 - (void) sayHello; //吃饭行为 - (void) eat; @end 代码说明: 1.#import<Foundation/Foundation.h>语句告诉编译器查看Foundation框架中的Foundation.h的头文件 2.用#import指令来导入相应的文件,#import的作用相当于PHP中的require_once,如果文件之前导入过了,则不导入,而#include会重复导入文件的 3.用编译器指令@interface来定义类的声明,@interface后面是类名,Student : NSObject 说明Student继承于NSObject类 4.在接口中方法只有声明,没有实现,方法前面的减号代表此方法是对象方法,如果是+号的话,说明是类方法,就是说类可以直接访问此方法。 5.@interface 和 @end是成对出现的,@end代表接口定义的结束 6.上面得成员变量定义成了公有的,这在开发中是极少见的,这里为了方便练习才这么写的,一般把成员变量定义为私有的然后在定义get,set方法去操作成员变量 这样才起到了封装,不要把自己的手直接伸入到类中,要通过类提供的方法来操作类的成员变量。 2.@implementation 实现部分 实现部分文件的扩展名为.m,具体实现的方法代码如下: #import "Student.h" @implementation Student //实现打招呼的行为 - (void) sayHello { NSLog(@"hello! 我是%@, 我今年%d岁了, 我喜欢%@!", studentName, age, hobby); } //实现吃饭行为 - (void) eat { NSLog(@"%@也得吃饭呢!",studentName); } @end 代码说明: 1.在实现文件中首先导入对应的类的接口文件#import "Student.h",引入系统文件用<FileName>, 引入自定义文件用“FileName”; 2.用编译器指令@implementation来声明类方法的实现 3.@implementation和@end也是成对出现的 代码规范: 1.类名的首字母要大写 2.方法的命名使用驼峰命名法 3.创建和使用对象 定义了一个类,一般需要实例化才能使用,当然静态类是不需要实例化就能用的。对象是类的实体,类是对象的抽象,因此需要对类进行实例化。 实例化的代码如下: #import <Foundation/Foundation.h> //引入定义的类 #import "Student.h" int main(int argc, const char * argv[]) { @autoreleasepool { //创建对象,完成对象的声明,内存分配,初始化 Student *student = [[Student alloc] init]; //为了方便,在这就直接给成员变量赋值,前提是成员变量是公有的 student->studentName = @"ludashi"; student->age = 20; student->hobby=@"爱咋咋地"; //调用成员方法 [student sayHello]; [student eat]; } return 0; } 代码说明: 1.要在一个类中实例化类,首先要引入类的接口定义文件如上面的 #import “Student.h”; 2.OC中类的实例化方式是 Student *student = [[Student alloc] init], 可以理解为Student类在NSObject类中继承了alloc方法, alloc这个方法是用来 实例化对象的 init 是默认的构造方法 3.在OC中对象调用其中的方法是通过[]来实现的,[对象名 方法名]; 定义对象的语法: 类名 *对象名 = [ [ 类名 alloc ] init ]; 或者 类名 *对象名 = [ 类名 new]; 给对象的成员变量赋值和调用对象的方法如下: 对象名->成员变量名 = 具体值; [ 对象名 方法名]; 一:类方法和对象方法 上面也提到了,减号开头的方法为对象方法,需要实例化类后通过对象来进行调用。对象方法允许调用对象方法和对象变量加号开头的方法为类方法,通过类就可以直接调用的方法。 下面是一些方法调用的一些规则: 1.类方法可以调用类方法; 2.类方法不可以调用对象方法; 3.类方法不可以使用对象变量,类方法可以使用self(self相当于java中的this); 4.可以通过类来调用类方法,但对象不可以调用类方法 1.对象的初始化 可以重写父类中的方法init来进行对象的初始化,就相当于Java中的构造函数,重写代码如下: //重写init方法,为本类添加构造方法 -(id) init { if (self =[super init]) { studentName = @"dashi"; age = 18; hobby = @"hehe"; } return self; } 代码说明: 1.因为init是继承过来的方法因此不需要在interface里面声明,直接在implementation中进行重写就OK了; 2. init的返回类型为id, id是OC中的一切父类.在面向对象中父类可以声明子类的变量 3.[super init]是为了初始化父类的成员变量,返回值为子类对象,如果返回nil,说明父类没有alloc成功,即不能在alloc子类对象。 2.便利初始化函数 自定义的便利初始化函数的作用是让用户自己初始化用户所实例化的对象,便利初始化函数以init开头,我们可以在类中自定义便利初始化函数。因为便利初始化函数是自 己。 定义的所以需要在@interface中进行声明,便利初始化函数可以进行重载 在@implementation中进行重新的代码如下: //实现便利初始化方法1 - (id) initWithName:(NSString *)sName iAge:(int)anAge { if (self = [super init]) { studentName = [sName copy]; age = anAge; } return self; } //实现便利初始化方法2 - (id) initWithName:(NSString *)sName { if (self = [super init]) { studentName = [sName copy]; } return self; } 代码说明: 1.函数名后面的冒号跟的是参数列表,在OC中定义参数的方式是: (参数类型)参数名 第二个参数别名:(参数类型)参数名 使用便利初始化方法来进行对象的初始化,代码如下: //调用便利初始化方法 Student *s1 = [[Student alloc] initWithName:@"dashi1" iAge:19]; [s1 sayHello]; 3.便利构造器 上面用便利初始化方法在类实例化时有些繁琐,为了简化实例化的操作,自定义一个类方法,类方法的作用是进行类的实例化同时进行参数的初始化,并返回对象 便利构造器的实现代码如下: //实现便利构造器,进行类的实例化并调用initWithName + (id) studentWithName:(NSString *)name age:(int)sAge { Student *student = [[Student alloc] initWithName:name iAge:sAge]; return student; } 便利构造器的使用如下: //调用便利构造器初始化对象 Student *s2 = [Student studentWithName:@"dashi2" age:20];