[Swift基础语法入门] Swift 字符串一口闷

觉得不错就关注我吧,不定期更新文章,QQ:1345614869

字符串初始化

1 最常用的几种方法

var emptyString = ""      // 初始化一个空字符串,这里注意空字符串 ≠ nil
var stillEmpty = String() // 使用init()来初始化一个字符串
let str = "Hello Swift!"  // 使用字符串字面量来初始化

2 使用特定字符串init()方法

let boolStr = String(true)      // 传入布尔类型 true or false -> "true" or "false"
let catCharacters:[Character] = ["C","a","t","!","🐱"]//  先声明一个字符数组
let catStr = String(catCharacters)// 通过传入字符数组初始化一个字符串
let floatStr = String(3.14) // 传入一个浮点类型数初始化一个字符串
let intStr = String(1234)   // 传入一个整型数初始化一个字符串
let unicodeStr = "\u{4e00}" // 传入unicode码转字符串,这是一个中文“一”
let rcStr = String(count: 2, repeatedValue: Character("x")) // 重复多个Character字符生成一个字符串
let ruStr = String(count: 2, repeatedValue: UnicodeScalar(0x4e00)) // 这里注意UnicodeScalar 可以使用8位 16位 32位

3 使用插值法构造字符串

这是我比较喜欢的语法之一,不像C语言需要使用诸如printf("number is %d",number)这种占位符,清爽很多。

let name = "pmst"
let age = 26
let info = "name is \(name),age is \(age)"

字符串实用方法

1.字符串的前缀和后缀判断

var psStr = "111 pmst 222"
psStr.hasPrefix("111") // 判断前缀 true
psStr.hasSuffix("222") // 判断后缀 true

2.字符串是否为空

var emptyStr = ""
emptyStr.isEmpty  // true 这是一个只读属性
emptyStr == ""    // true

3.获得字符串的大小写

var ulStr = "aBcDeF"
let upper = ulStr.uppercaseString // "ABCDEF" 这货是一个只读属性
let lower = ulStr.lowercaseString // "abcdef" 同上

4.字符串的存储方式

常常我们会问一个字符串到底有多少个字符,不如看看这个特殊的例子:

// 其中café中的e是带第二声调的。其中\u{E9} 就是é
let unicodeString = "caf\u{E9}"     // "café"

// swift中为我们提供了 
// 1. characters(String.ChacracterView类型) 
// 2. unicodeScalars 21位编码,官方博客有篇文章说了
// 3. utf16
// 4. utf8  oc貌似是这货
unicodeString.characters.count      // 4
unicodeString.unicodeScalars.count  // 4
unicodeString.utf8.count            // 5
unicodeString.utf16.count           // 4

那不禁要思考下é 是否可以看成 e + 声调呢? 答案显然是ok!

let anotherCafeString = "caf\u{65}\u{301}" // "café"

anotherCafeString.characters.count         // 4
anotherCafeString.unicodeScalars.count     // 5
anotherCafeString.utf8.count               // 6
anotherCafeString.utf16.count              // 5

可以看到两者还是不同的!<##原因待补充##>

5.遍历

Swift 中我们并不能将字符串看做是一个Character类型的数组,因此str[1] 这种传入整数值下标取值的方式是不被允许的,但是!如果你用Index类型下标取值是可以的!请见下面内容。

除此之外我们需要讨论下关于一些特殊的字符串,譬如"café"中的"é"是看成一个字符呢?还是两个字符(韵母+声调)?或许你已经发现 Swift 贴心地帮你处理了一切,是作为一个整体存在,即类型为String.ChacracterView

扯了那么多开始本小节的话题,如何遍历字符串:

var cafeSentence = "café"

for c in cafeSentence.characters{
    /*
        依次输出:
        character in cafe Sentence is c
        character in cafe Sentence is a
        character in cafe Sentence is f
        character in cafe Sentence is é
    */
    print("character in cafe Sentence is \(c)")
}

使用for-in方式遍历当然是一个不错的选择,但是如果想要从特定位置开始获取某个字符串片段又该如何实现呢?这时候需要引出两个关键属性了!

  • startIndex:非空字符串的首个元素位置。
  • endIndex:非空字符串的结束元素位置的后一个位置!这里一定要记住!!

不妨看看 Swift 中是怎么定义这两货的:

extension String {
    public typealias Index = String.CharacterView.Index
    /// The position of the first `Character` in `self.characters` if
    /// `self` is non-empty; identical to `endIndex` otherwise.
    public var startIndex: Index { get }
    /// The "past the end" position in `self.characters`.
    ///
    /// `endIndex` is not a valid argument to `subscript`, and is always
    /// reachable from `startIndex` by zero or more applications of
    /// `successor()`.
    public var endIndex: Index { get }
    
    // 注意这里!其实是有下标取值的 但是传入类型要是Index类型
    public subscript (i: Index) -> Character { get }
}

先测试下startIndexendIndex的值,方便理解:

var cafeStr = "café"
cafeStr.startIndex  // 0
cafeStr.endIndex    // 4 而不是3哦!

别被 0 、4误导,这两个参数的类型是String.CharacterView.Index,作为一个Struct,它们包含了2个方法:

public struct Index : BidirectionalIndexType, Comparable, _Reflectable {
    /// Returns the next consecutive value after `self`.
    ///
    /// - Requires: The next value is representable.
    public func successor() -> String.CharacterView.Index
    /// Returns the previous consecutive value before `self`.
    ///
    /// - Requires: The previous value is representable.
    public func predecessor() -> String.CharacterView.Index
}

其中:

  • successor() 方法是取到下一个字符的索引值Index
  • predecessor() 方法是取到前一个字符的索引值Index

不妨看看String.UFT16View.Index 的 extension 内容

extension String.UTF16View.Index {
    @warn_unused_result
    public func distanceTo(end: String.UTF16View.Index) -> Distance
    @warn_unused_result
    public func advancedBy(n: Distance) -> String.UTF16View.Index
    @warn_unused_result
    public func advancedBy(n: Distance, limit: String.UTF16View.Index) -> String.UTF16View.Index
}

这里需要了解advanceBy(n:Distance)方法,其中Distance是这样的:public typealias Distance = Int,因此也木有神秘感。

讲了那么多,来稍微实践下:

cafe[cafe.startIndex]                         // "c"
cafe[cafe.startIndex.successor()]             // "a"
cafe[cafe.startIndex.successor().successor()] // "f"

// 前面说endIndex是最后一个字符的还要后面一个位置
// 因此cafe[endIndex] 会报错哦 就是数组越界嘛!
cafe[cafe.endIndex.predecessor()]             // "é"
cafe[cafe.startIndex.advancedBy(2)]           // "f"

如果你觉得用successor()以及predecessor()方法向前或向后取索引值麻烦的话,不妨用用swift为你提供的indeces属性,类型为:Range<Self.Index>,不是 NSRange !

// 通过这种方式我们先获取的是每个字符的索引值,而非字符
for index in cafe.characters.indices {
  print(cafe[index])
}

Swift String Cheat Sheet 中说我们不能将某个字符串的索引用于另外一个字符串的索引值来取字符,但是我测试时候没问题的,这里大家可以讨论下。

let word1 = "ABCDEF"
let word2 = "012345"
let indexC = word1.startIndex.advancedBy(2)
let char_2 = word2[indexC]                                               // "2"
let distance = word1.startIndex.distanceTo(indexC) // 2 指代第一个索引距离第二个索引的距离
let digit = word2[word2.startIndex.advancedBy(distance)] // "2"

使用 Range 来截取字符串片段

实际开发中经常遇到这样的需求,截取某个字符段片段,然后设置特定的属性,譬如颜色,字体大小等等。

此时就要用到范围Range这一术语了,比如这么描述一个范围,获取字符串第二个字符开始到字符串倒数第二个字符为止的中间所有内容。

不妨试试:

let swiftGG = "www.swift.gg"
let rangeOfSwift = Range(start: swiftGG.startIndex.advancedBy(4), end: swiftGG.endIndex.advancedBy(-3))   // 4..<9 = [4,9) 左闭右开
let swift = swiftGG[rangeOfSwift] // "swift"

其实吧,我们也可以使用“...”和“..<”构建一个Range,注意前者是闭区间,后者是左闭右开,换句话说startIndex...endIndex 等价于 startIndex..<endIndex.successor()

为了更清楚地方便大家理解,我打算再啰嗦几句,Range始终构造一个[s,e)的区间,正如你所看到的是左边闭合,右边开区间。即使使用"..."也是如此,不如测试下:

1...4  //1..<5 很神奇吧 不过呢这是Range<Int> 而非Range<Index>类型
swiftGG.startIndex ... swiftGG.endIndex.predecessor() // 0..<12

因此倘若你要截取一个字符串中的某个片段,你要把头指向要截取字符串的首字符之上,把尾指向要截取字符串的尾字符的后一个位置

获取字符串子串的方法

前面我们是通过构建一个Range范围,下标取子串的方式实现,这里我们介绍另外几个实用的方法:

  • substringFromIndex(index:Index),获取[index,endIndex)之间的子串
  • substringToIndex(index:Index),获取[startIndex,index)之间的子串
  • substringWithRange(range:Range),获取range范围的子串
let rangeStr = "abcdefg"
rangeStr.substringFromIndex(rangeStr.endIndex.predecessor())
rangeStr.substringToIndex(rangeStr.endIndex.predecessor())

处理字符串的前缀和后缀字符

有时候免不了要处理下字符串的前缀和后缀,比如去头去尾拉等等,然后生成一个新的字符串,《Swift String Cheat Sheet》一文的例子已经非常地描述了一切,因此我直接引用过来

let digits = "0123456789"
let tail = String(digits.characters.dropFirst())  // "123456789"
let less = String(digits.characters.dropFirst(3)) // "3456789"
let head = String(digits.characters.dropLast(3))  // "0123456"

let prefix = String(digits.characters.prefix(2))  // "01"
let suffix = String(digits.characters.suffix(2))  // "89"

let index4 = digits.startIndex.advancedBy(4)
let thru4 = String(digits.characters.prefixThrough(index4)) // "01234"
let upTo4 = String(digits.characters.prefixUpTo(index4))    // "0123"
let from4 = String(digits.characters.suffixFrom(index4))    // "456789"

主要还是对characters的操作,简要介绍下出现的几个方法:

  • dropFirst() 去掉首字符,返回剩余字符串
  • dropFirst(n) 去掉首部开始n个字符,返回剩余字符串
  • dropLast(n) 同理,但是从尾部去除n个
  • prefix(n) 获得字符串前n个字符
  • suffix(n) 获得字符串后n个字符
  • prefixThough(Index) 获取[startIndex,Index]的字符串子串
  • prefixUpTo(Index) 获取[startIndex,Index)的字符串子串
  • suffixFrom(Index) 获取[Index,endIndex)的字符串子串

总结来说,不是很难,用多了自然熟。

插入

插入操作无非就是在某个位置(Index)插入一个字符或者一个片段喽

var insertStr = "abcdefg" //"abcdefg"
insertStr.insert(Character("x"), atIndex: insertStr.startIndex) //"xabcdefg"
insertStr.insertContentsOf("xyz".characters, at: insertStr.startIndex)

注意是在给定索引值的前面开始插入!

删除

删除操作无非就是删除特定位置(Index)字符和特定范围(Range)字符串。

var deleteStr = "abcdefg"

// 删除特定位置
deleteStr.removeAtIndex(deleteStr.startIndex) // 返回删除的"a"
deleteStr       // "bcdefg"  
deleteStr.removeRange(deleteStr.startIndex..<deleteStr.endIndex.predecessor().predecessor()) // "fg"
deleteStr // "bcde"

是时候要强调下,删除和插入都是会改变原数组的!替换也是一样的!

替换

替换就是将某个范围的内容替换成传入的内容

var replaceStr = "abcdefg"
replaceStr.replaceRange(replaceStr.startIndex..<replaceStr.endIndex.advancedBy(-2), with: "!!!")  
replaceStr  //!!!fg

这里注意替换范围内的元素个数和即将替换的元素个数是不一定要匹配的!

附加

这个用的比较多,通常来说“+”操作即可,不过使用appendappendContentsOf,这里不得不提的就是我们在使用代码方式设置视图约束时,就有这个概念了,不妨去翻翻?

// 引自Swift String Cheat Sheet例子
var message = "Welcome"
message += " pmst" // "Welcome Tim"
message.appendContentsOf("!!!") // "Welcome pmst!!!

找出字符串中的 emoji 字符

首先定义emoji表情的范围,哪些需要,哪些忽略,前面说到字符的编码方式有三种,这里主要谈及scalar,21位编码方式,譬如😃对应了0x1F601,最后通过union我们获得到了一个集合,集合内元素是不重复的! 通过遍历String字符串中的每一个字符(characters为字符串所有字符)并使用filter过滤函数判断是否包含在早前获得emoji集合中,将符合的元素留下返回。

// 获取字符编码对应的数值
extension Character{
    // 取得对应的值
    private var unicodeScalarCodePoint:Int{
        let characterString = String(self)
        let scalars = characterString.unicodeScalars    // 貌似是21位的编码字符集?
        return Int(scalars[scalars.startIndex].value) // 先通过startIndex获取 然后获取到Int32值
    }
}

extension String{
    var emojis : [Character]{
        let emojiRanges = [0x1F601...0x1F64F, 0x2702...0x27B0]
        // 将上述的范围变成一个集合
        let emojiSet = emojiRanges.reduce(Set<Int>(), combine: {
            (result, elem) -> Set<Int> in
            // union方法是并集的意思 即集合(1 3 5 7)和集合(0 2 4 6) 操作后为(0,1,2,3,4,5,6,7)
            return result.union(elem)
        })
        // 使用filter过滤
        return self.characters.filter{
            emojiSet.contains($0.unicodeScalarCodePoint)
        }
    }
}

String 和 NSString 的桥接使用

TODO: 整理中

参考文献

推荐阅读更多精彩内容