美文网首页
字符与字符串

字符与字符串

作者: 曹来东 | 来源:发表于2018-09-26 16:48 被阅读10次

    在Swift编程语言中,字符与字符串的字面量都是用双引号围起来的字符序列构成的。所以如果我们要表达一个字符字面量,那么必须显式使用 Character 类型进行声明,比如:Character("你")。默认情况下,双引号围起来的字符序列字面量为字符串字面量。比如:"a"、"hello, world"、"你好吗?" 这些都属于字符串字面量。注意,用单个双引号包围的字符串字面量中不能出现换行符。
    在Swift 4.0中引入了多行字符串字面量的表达形式,通过用 """ 三个双引号包围起来的字符串段落来表示。在字符串段落中允许出现多个多个换行符,比如:

    """
    
    今天天气很不错,我出去逛逛。
    
    It is a good day today. I’m going to have a walk.
    
    """
    

    不过我们要注意到是,在一般情况下,多行字符字面量中的单个换行符在段落的首尾中是被忽略的。就如上述多行字符串字面量中,输出结果中第一句的前面以及最后一句的末尾是没有换行符的,而中间有换行符的,则输出字符串中也有换行符。如果我们要对首尾添加换行符的话有两种形式,一种是通过我们将在第2节中会描述的转义字符;另一种是添加一个额外的换行。此外,如果我们在段落中不想有一个换行符,但因为文字可能比较长,所以在书写时希望有一个换行,此时我们可以在一行的末尾添加 \ 符号,然后紧跟换行符即可。

    """
    
     
    今天天气很不错,我出去逛逛。\
    
    It is a good day today. I’m going to have a walk.\
     
    """
    

    上述代码中,字符串的第一句前面以及最后一句的末尾各有一个换行符,而中间句子之间没有任何换行符。
    此外,多行字符串字面量可以控制每行内容的缩进。如果在末尾 """ 之前没有输入任何空格符,那么每行起始的所有空格符将在结果执行字符串中保留;如果在末尾 """ 之前有N个空格符,那么在每行开头的前N个空格符将被忽略。我们再看以下例子:

    """
    今天天气很不错,我出去逛逛。
    It is a good day today. I’m going to have a walk.
    今日は良い天気ですから、散歩しましょう〜
    """
    

    上面这段字符串的每一句开头都不会有额外的空白字符,也就是说,每一行的开头均与最后的 """ 的缩进对齐。当然,如果这里开头的 """ 具有缩进,那么最后的效果也一样。
    字符类型刚才提到了,用 Character 表示;字符串类型则是用 String 来表示。字符类型与字符串类型都属于结构体类型。

    字符概述

    Swift编程语言中,对字符的表示是独立于某一种特定的Unicode编码的。在Objective-C、Java等编程语言中,编译器内部一般以UTF-16编码格式保存并处理字符序列的,而在Swift编程语言中则直接通过Unicode标准组织所制定的标准码点(code point)来保存并处理字符串字符序列。Swift 4.0则直接引入了 Unicode 枚举类型作为操作Unicode字符及字符串的工具,然后将Unicode码点值定义为Unicode标量值(Unicode scalar values),用 Unicode.Scalar 结构体类型表示。所以Swift中引入了 Unicode.Scalar 类型,直接可用于表示某一字符的码点值。此外,为了更简洁地表示一个Unicode字符,Swift 4.0将 Unicode.Scalar 定义为了 UnicodeScalar 类型。
    对于UTF-8、UTF-16这些编码格式而言,都是根据Unicode定义好的码点值,然后再加上自身的编码规则构成的。一个UTF-8编码的字符可由1到4个字节构成;而一个UTF-16编码的字符可由2个或4个字节构成。它们都属于变长编码。
    对于整个可用的Unicode补充平面而言,一个码点值一共占用20个有效比特位,而在Swift编程语言中,使用了21个比特来表示一个Unicode标量值。我们也可以用一个字符串字面量对一个 UnicodeScalar 类型的对象赋值,这样我们可很方便地查询到当前字符的Unicode标量值是多少。此外,一个字符对象也能直接通过一个 UnicodeScalar 对象进行构造。下面我们来举一些简单的例子:

    // 这里直接通过UnicodeScalar的构造方法来创建一个UnicodeScalar常量对象zero,
    // 这里要注意的是,
    // 通过UnicodeScalar构造出来的对象是一个Optional对象
    let zero = UnicodeScalar("0")!
     
    // 这里使用类型标注的方式显式给常量a声明为UnicodeScalar类型,
    // 然后用"a"字符串字面量对它初始化
    let a: UnicodeScalar = "a"
     
    // 声明UnicodeScalar常量π
    let π = UnicodeScalar("π")!
     
    // 声明UnicodeScalar常量好
    let 好: UnicodeScalar = "好"
     
    // 输出zero的标量值:48(与ASCII码兼容)
    print("\(zero) scalar value = \(zero.value)")
     
    // 输出a的标量值:97(与ASCII兼容)
    print("\(a) scalar value = \(a.value)")
     
    // 输出π的标量值:960(与UCS-2兼容)
    print("\(π) scalar value = \(π.value)")
     
    // 输出好的标量值:22909(与UCS-2兼容)
    print("\(好) scalar value = \(好.value)")
     
    let 😄: UnicodeScalar = "😄"
     
    // 输出😄的标量值为128516(在补充平面中)
    print("\(😄) scalar value = \(😄.value)")
    

    我们看到,在上述代码中用的都是 UnicodeScalar 类型的对象,似乎光UnicodeScalar 就能解决一切了,但是为何还要 Character 类型呢?对于现代充斥着丰富的Emoji的时代而言,光20位的码点已经无法满足需求了,许多Emoji都已经用了两个码点值来表示了,比如各国国旗。所以,Swift编程语言特意使用了 Character 类型以覆盖所有可被编码的字符。这也意味着一个 Character 对象可包含多个 UnicodeScalar 对象,这在Swift中称为扩展字母簇(extended grapheme clusters)。各位还要注意的是,扩展字母簇中所包含的每个Unicode标量值也都是合法有效的Unicode码点,然而当它们被按照一定次序摆放在一起时就会构成另一种字符。这就好比某些编辑文档中所使用的连接字(比如将两个 - 放在一起会被组成 — )。下面举一个例子:

    // 我们这里的flag对象使用UnicodeScalar声明,
    // 那么编译器就会报错,
    // 因为🇺🇸需要两个UnicodeScalar对象来表示
    let flag: Character = "🇺🇸"
     
    // 这里会打印出:
    // flag is: 🇺🇸
    print("flag is: \(flag)")
     
    // 光用Character类型,我们无法获取其码点值,
    // 所以我们这里借助String.UnicodeScalarView类型
    let scalars = "🇺🇸".unicodeScalars
    print("scalars:", separator: "", terminator:" ")
     
    // 我们这里可以发现一个很有意思的情况:
    // 🇺🇸正好是由两个Emoji字符构成:🇺和🇸
    for scalar in scalars {
    print("\(scalar)=\(scalar.value)", separator: "", terminator: " ")
    } 
    我们上面直接在字符串字面量中通过Emoji表情符号来表示出美国星条旗🇺🇸。不过各位也可以通过码点值来构造 UnicodeScalar 类型的对象,然后通过字符串对象转换为字符对象。我们下面将以中国五星红旗🇨🇳的Emoji字符作为例子:
    // 这里直接通过码点值来创建UnicodeScalar对象
    let c = UnicodeScalar(127464)!
    let n = UnicodeScalar(127475)!
     
    // 这里的UnicodeScalar常量c输出的是C的Emoji;
    // 这里的UnicodeScalar常量n输出的是N的Emoji;
    print("c = \(c), n = \(n)")
     
    // 我们将两个UnicodeScalar对应的字符对象拼在一起,
    // 构成一个String对象,
    // 然后用Character构造方法将此String对象中的内容转换为Character相应的值
    let cn = Character(String([Character(c),Character(n)]))
     
    // 这里我们就能看到中国国旗的Emoji了
    print("cn = \(cn)")
    
    

    转义字符

    在任何编程语言中都会存在一些无法显示在编辑器中的字符(比如空字符),或者与编程语言中表示字符串的语法产生冲突的字符(比如双引号、换行符等等)。所以大部分编程语言都会引入转义字符来表示这些无法在源代码中所表示的特殊字符。
    对于与ASCII码兼容的转义字符,Swift编程语言中使用倒斜杠作为转义字符的引导前缀,然后由此定义了以下特殊字符:
    "\0" 表示空字符(null character),一般用于传统ASCII编码的字符串的结束符。
    "\" 表示倒斜杠(backslash)。
    "\t" 表示水平制表符(horizontal tab)。
    "\n" 表示换行(line feed)。
    "\r" 表示回车(carriage return)。
    """ 表示双引号。
    "'" 表示单引号。当然,一般单引号在Swift编程语言中表示字符或字符串字面量时不需要做转义,直接用 "'" 也行。
    在多行字符串字面量中,如果遇到一单个双引号 ",那么可不必使用转义字符,可直接写。但如果想表示连续三个双引号的话,那么我们至少得对其中一个使用转义字符。
    Swift编程语言中引入了可表达任一Unicode标量值的转义语法—— \u{n}。这里的 n 是一个1到8位的十六进制数,并且它的值必须能表示有效的Unicode码点。以下例子例举了一些转义字符的使用。

    /**
    * 这里将会输出:
    The string is: Say, "Hello, world"!
    非常好!
    * 注意双引号与换行符的输出
    */
    let s = "Say, \"Hello, world\"!\n非常好!"
    print("The string is: \(s)")
     
    let a = "🇺🇸".unicodeScalars.first!.value
    let b = "🇺🇸".unicodeScalars.last!.value
     
    print("a = \(a), b = \(b)")
     
    // 这里的flags字符串由一个🇺🇸与一个🇨🇳构成。
    // 这里用的就是通过\u{}转义字符来指定Unicode码点值
    let flags = "\u{1f1fa}\u{1f1f8}\u{1f1e8}\u{1f1f3}"
    print("flags: \(flags)")
    // 这里通过插入空字符将上面四个Unicode标量值分隔开,
    // 使得它们可以作为单独的一个Unicode字符显示出来
    let chars = "\u{1f1fa}\0\u{1f1f8}\0\u{1f1e8}\0\u{1f1f3}"
    print("chars: \(chars)")
     
    // 我们这里能看到,chars字符串一共有7个字符
    print("chars count: \(chars.characters.count)")
    

    如果在一个字符串字面量中,在倒斜杠后面跟着其他字符,而不是上述所列出的那几个,那么编译器就会直接报错。此外,倒斜杠后面还能跟一对圆括号,()是一种特殊语义,这在之前的代码示例中也已经用到了.

    字符串概述

    一个字符串是一组字符序列,在Swift中使用 String 类型来表示一个字符串类型。现在大部分面向对象的编程语言不是单单将字符数组类型作为字符串类型,因为将字符串类型独立拿出来会带来许多额外的便利。就Swift编程语言而言,String 类型不仅提供了对它所包含的字符序列的封装,而且还有将它们转为UTF-8编码以及UTF-16编码的独立Unicode视角类型。这得益于Swift中的字符以中立的Unicode码点为基础来表示的缘故。我们要创建一个字符串对象,可以通过丰富的 String 类型的构造方法,也可以直接通过字符串字面量。我们看以下例子:

    // 这里用空字符串字面量创建一个字符串变量empty
    var empty = ""
     
    // 这里用默认字符串构造方法来创建一个空的字符串对象
    empty = String()
     
    // 这里用常用的字符串字面量来初始化一个字符串常量
    let str = "hello"
    // 这里直接通过指定字符序列的字符串构造方法创建一个字符串常量
    let str2 = String("hello")
     
    // 这里通过指定一个字符的字符串构造方法来创建一个字符串常量
    let ch = String(Character("字"))
    

    字符串与之前的收集类型类似,也分为可修改的(mutable)字符串对象与不可修改的(immutable)字符串对象。使用 var 声明的字符串对象是可修改的;使用 let 声明的字符串对象是不可修改的。此外,字符串字面量本身是不可修改的。
    我们下面谈谈字符串的实例方法与属性。字符串与数组对象类似,也可用 + 操作符将两个字符串对象拼接起来,构成一个新的字符串对象。对于一个可修改的字符串对象,则可使用 += 操作符将 += 右边的字符串的内容添加到 += 左边的可修改的字符串对象的末尾。此外,我们还能直接通过 == 操作符来比较两个字符串对象中的字符序列内容是否完全相同,如果相同则返回 true,否则返回 false。而要判断一个字符串是否为空,则可使用 isEmpty 实例属性。判定字符串是否包含指定的子串,则可使用 contains(:) 实例方法。如果我们要判断一个字符串对象的开头是否包含某个子串,可以使用 hasPrefix(:) 实例方法;如果我们要判断一个字符串对象的末尾是否包含某个子串,那么可以使用 hasSuffix(_:) 实例方法。下面我们举一些例子来说明:

    // 这里通过 + 操作符将两个字符串字面量拼接在一起构造出一个新的字符串对象给常量a
    let a = "Hello, " + "world!"
     
    // 字符串常量b则是将两个"Hello, world!"中间用换行符连接在一起
    let b = a + "\n" + a
    print("b = \(b)")
     
    // 比较字符串对象a与"Hello, world!"是否相同
    // 比较结果为true
    print("Is equal? \(a == "Hello, world!")")
     
    // 判定字符串对象是否为空字符串
    // 结果为false
    print("Is empty? \(a.isEmpty)")
     
    // 判断是否包含 , 符号
    // 结果为true
    print("contains ','? \(a.contains(","))")
     
    // 判断字符串对象a是否含有前缀"Hello"
    // 结果为true
    print("Has prefix 'Hello'? \(a.hasPrefix("Hello"))")
    // 判断字符串对象a是否含有后缀"d!"
    // 结果为true
    print("Has suffix 'd!'? \(a.hasSuffix("d!"))")
     
    // 这里声明了字符串变量str,
    // 并将a的内容对它初始化
    var str = a
     
    // 在str对象后添加一个字符串内容
    str += " 你好,世界!"
    print("str = \(str)")
    

    字符串插值

    Swift编程语言提供了一种十分便利的方式可将几乎任一类型的对象转换为一个字符串的表示形式,这在Swift中称为字符串插值(String Interpolation)。字符串插值的语法非常简单,就是在一个字符串字面量中使用(object)。我们可以看到,字符串插值也是用了转义字符的语法,只不过这里不能称为转义字符,因为我们要替换的是一组字符序列。
    我们之前已经涉及到了字符串插值,比如我们使用print函数来打印信息的时候就往往会使用字符串插值。对于我们自定义的类型,也可以使用字符串插值,Swift会生成对应的类型描述字符串,下面我们来看一些例子:

    let x = 10, y = 20
     
    // 这里声明了一个字符串常量s,
    // 并且用字符串插值的形式将x + y的结果表示在字符串字面量中,
    // 为s进行初始化
    let s = "x + y = \(x + y)"
     
    // 这里也是用字符串插值的方式将s的内容打印出来
    print("s is: \(s)")
    // 当然,因为s本身是字符串类型,
    // 所以我们也可以直接用字符串拼接方式来打印
    print("s is: " + s)
     
    struct MyType {
    var x = 10, y = 20
    }
     
    // 我们这里自己定义了一个结构体类型MyType,
    // 然后这里依然可以使用字符串插值方式输出MyType的对象所对应的字符串表达
    let str = "my type: \(MyType())"
     
    // 这里输出:my type: MyType(x: 10, y: 20)
    print(str)
     
    

    对于一些特殊场合,我们可能需要为自己定义的类型定制字符串表达描述。此时,我们可以通过遵循 CustomStringConvertible 协议,然后实现 description 计算型只读属性来达成。下面举一个简单例子:

    /// 这里的MyType遵循了CustomStringConvertible协议,
    /// 并实现了description只读计算型属性
    struct MyType: CustomStringConvertible {
    var a = 10
    var b = 20
    var description: String {
    return String("MyType(\(a + b))")
    }
    }
     
    var obj = MyType()
    obj.a = -obj.a
    obj.b *= 2
     
    // 这里的字符串插值将会使用MyType类型中的description计算型只读属性所返回的字符串内容
    let str = "My type is: \(obj)"
     
    // 这里输出:My type is: MyType(30)
    print(str)
    

    字符串的characters属性

    如果我们想要迭代访问一个字符串中的每个字符,或者是查询当前字符串中包含多少字符个数,那么我们可以访问 String 对象的 characters 属性。各位要注意的是,这里的 characters 属性并不是 [Character] 类型,而是 String.CharacterView 类型。尽管 String.CharacterView 类型遵循了 Sequence 协议,使得我们可以使用 [for-in 循环迭代语句]将其中的字符一一枚举出,但是我们无法使用整数作为下标索引来访问 characters 中的指定字符。我们需要使用字符串专用索引的方式进行访问。

    为何 characters 属性需要单独搞一个 String.CharacterView 类型,而不是直接使用 [Character] 类型呢?因为我们之前在第1节就提到了,Swift编程语言对字符采用了扩展字母簇的形式,这意味着在一个字符序列中的某一位置插入一个新的字符之后,字符序列长度未必会有所变化,一旦前后两个字符对应的Unicode标量值被合并在一起,拼接成一个字符对象,那么该字符串的总长度仍然不变。因此,Swift的基础库并不是简单粗暴地使用 [Character] 类型。下面我们来看一个示例:

    var str = "看旗帜:\u{1f1e8}"
    // 我们先观察当前str对象的长度,
    // 长度为5
    print("str count: \(str.characters.count)")
     
    print("before appending:", separator: "", terminator: " ")
     
    // 我们输出当前str中的字符序列
    for ch in str.characters {
    print("\(ch)", separator: "", terminator: " ")
    }
     
    print("")
     
    // 我们再新增一个字符
    str.append("\u{1f1f3}")
     
    // 输出当前字符串中字符个数,
    // 仍然为5
    print("str count: \(str.characters.count)")
     
    print("After appending:", separator: "", terminator: " ")
     
    // 最后,我们再逐一输出字符序列
    for ch in str.characters {
    print("\(ch)", separator: "", terminator: " ")
    }
    

    上面我们可以看到,在插入了🇳的Emoji之后,字符串对象str的长度仍然是5。同时,原本最后一个🇨的Emoji字符与后来插入🇳的Emoji字符拼接成了一个🇨🇳的Emoji字符。
    String.CharacterView 类型中包含了 first 属性,可用于获取当前字符序列中的第一个字符;另外还有 last 属性,可用于获取当前字符序列的最后一个字符。由于当前字符串可能是一个空串(即 ""),所以这两个属性的返回类型都是Optional。如果当前字符串是一个空串,那么这两个属性的值都是为空(nil)。我们下面来看以下代码示例:

    let str = "看旗帜:\u{1f1e8}\u{1f1f3}"
     
    var ch = str.characters.first!
     
    // 这里输出:看
    print("first character: \(ch)")
     
    ch = str.characters.last!
     
    // 这里输出🇨🇳的Emoji
    print("ch = \(ch)")
    

    字符串的索引及字符访问

    Swift编程语言为 String 类型引入了 索引类型 String.Index 用于表示字符串对象中字符序列的索引值。由于 String.Index 类型也遵循了 Comparable 协议,所以它可以作为范围操作符的操作数。
    由于 String.Index 类型它也是个结构体类型,本质上是对 String.CharacterView 字符索引的一个封装,所以我们无法直接拿来获取某一字符的位置。不过 String 类型中含有一个 startIndex 实例只读属性,表示当前字符串的起始索引位置。我们可以借助这个属性来计算导出其他索引值。另外,String 类型中还包含了 endIndex 属性表示当前字符串的末尾索引,我们可以用于判断当前计算的索引是否超出了最大索引范围,因为刚才提到了,String.Index 类型遵循了 Comparable 协议,因此可直接比较两个索引对象的大小。这里要注意的是,endIndex 属性指定的是字符串中最后一个字符的后一个索引位置,所以该属性值的前一个位置才是此字符串中的最后一个字符的位置。
    我们通过调用字符串对象的 index(:offsetBy:) 实例方法即可得到指定的索引值。该方法的第一个参数填参考索引值,第二个参数填指定的偏移 n,偏移值为正数,说明返回的索引值往后挪 n 位,如果是负数,则说明往前挪 n 位。此外, String 类型还有一个 index(:offsetBy:limitedBy:) 实例方法,功能与 index(:offsetBy:) 一样,只不过这里多了一个参数,第三个参数用于做边界检测,如果索引计算的结果超出了第三个参数所指定的值,那么此方法将会返回空。因此它返回的是一个Optional对象。
    有了索引值之后,我们就可以拿它通过字符串的下标操作符获取字符串的指定子串,然后通过子串的 characters 属性中的 first 属性或 last 属性来获取到当前指定的字符了。这里要展示给各位的是,在Swift 4.0中,字符串的下标操作符返回的是 Substring 类型,另外像 String 类型的 prefix(
    :) 实例方法以及 suffix(_:) 实例方法也都是返回 Substring 类型的子串对象。 Substring 类型对象是一个优化过的子串对象模型,它不仅具有 String 类型几乎全部的属性和方法,而且它共享了其原始字符串的存储空间,使得对它的操作非常快捷。我们下面来看一个例子:

    let str = "看旗帜:🇨🇳"
     
    // str字符串对象的起始索引,索引值相当于0
    let startIndex = str.startIndex
     
    // str字符串对象的末尾索引,索引值相当于:
    // str.characters.count
    let endIndex = str.endIndex
     
    // index2作为索引2
    let index2 = str.index(startIndex, offsetBy: 2)
     
    // index3作为索引3
    let index3 = str.index(startIndex, offsetBy: 3, limitedBy: endIndex)!
     
    // 这里通过范围操作符构造一个Range<String.Index>对象,
    // 作为字符串下标操作符的参数,以获取子串。
    // 这里substr的类型为Substring类型
    var substr = str[index2 ..< index3]
     
    // 我们这里可以观察到,
    // substr.characters的first字符与last字符都是同一个,
    // 即“帜”这个字。
    var ch = substr.characters.first!
    print("ch = \(ch)")
     
    ch = substr.characters.last!
    print("ch = \(ch)")
     
    // 这里以index3作为参考索引,
    // 然后偏移-2表示往前移2个位置,
    // 所以index1的索引值对应的是1
    let index1 = str.index(index3, offsetBy: -2)
     
    /// 这里以endIndex作为参考索引,
    /// 它表示字符串最后一个元素再加1,
    /// 因此这里的index4实际是对倒数第二个字符的索引,
    /// 即指向中文全角的冒号
    let index4 = str.index(endIndex, offsetBy: -2)
     
    // 获取第二到倒数第二个字符
    substr = str[index1 ... index4]
     
    // 这里输出:
    // 子串为:旗帜:
    print("子串为:\(substr)")
    

    相关文章

      网友评论

          本文标题:字符与字符串

          本文链接:https://www.haomeiwen.com/subject/ylzroftx.html