swift基础知识

一.常量和变量

[代码]
let maximumNumberOfLoginAtAttempts = 10    #用let声明常量
var currentLoginAttempt = 0  #用var声明变量

[类型注释]
 声明常量或者变量的时候提供类型注释,明确存储值的类型。常量或者变量名后依次添加 冒号,空格,类型名称来实现。
 var welcomeMessage: String
 welcomeMessage = "Hello"

 其实,可以不适用类型注释,因为在给常量或者变量提供初始值的时候,swift可以推测出他的类型。

[打印]
  println(welcomeMessage)   #打印变量
  #字符串插值 用“ \(变量名) ” 来占位,swift会用变量的值来替换这个占位符
  println("The current value of welcomeMessage is \(welcomeMessage)")

二.注释和分号

 [注释]
  //单行注释
  /*多行 注释*/

 [分号]
    语句结束后不需要;去标志结束
    但是如果多个语句写在一行,可以使用分号去分割

三.整数和浮点数

[整数]
  swift提供 8   16   32   64位有符号和无符号的整数
  Eg.
  UInt8  8位无符号整数
  Int32   32位有符号整数
 
  let minValue = UInt8.min
  let maxValue = UInt8.max

  在swift中,UInt ,  Int类型 与 当前系统的字长相等,
  32位系统,Int === Int32
  64位系统,Int === Int64

 [浮点数]
    Double  64位浮点数
    Float      32位浮点数

四.类型安全和类型推断

  类型安全:编译的时候会进行类型检查,标记不匹配的类型。
  类型推断:根据初值推断类型
 [数字的字面量]
    不带前缀的十进制数
    0b前缀的二进制数
    0o前缀的八进制数
    0x前缀的十六进制数
 [指数]
    1.25e2    -->   1.25 * (10^2)     #10进制基数是以10为底
    1.25e-2   -->   1.25 * (10^-2)
    0xFp2     -->    15 * (2^2)         #16进制基数是以2为底
 [数值类型的转化]
   整数间的转化demo:
   let twoThousand: UInt16  = 2_000   #_不会改变字面值,辅助提高可读性
   let one: UInt8 = 1
   let twoThousandAndOne = twoThousand + UInt16(one)   #加号两边都是UInt16可以相加

   浮点数的转化demo
   let three = 3
   let pointNum = 3.1415
   let pi = Double(three) + pointNum
   let intPI = Int(pi)

   上面这种SomeType(ofInitialValue) [eg. Double(three) ]
   是一种调用swift类型构造器的默认方法,我们为他传入初值,比如 UInt16 的构造器可以用来接收UInt8类型的值,
   所以可以将UInt8类型的值转化成UInt16,此处必须传UInt16支持的类型,不是随便什么类型都可以的。当然可以通过扩展让构造器支持自定义类型。

  [类型别名]
    typealias AudioSample = UInt16
    var maxUInt16Num = AudioSample.max    #UInt16.max

五.布尔值

 [声明和初始化]  
  let oragesAreOrange = true     #不需要制指定具体的Bool类型,编    译器会帮助我们推断的
  let oragesAreOrange = false

  [判断语句与Bool]
  错误写法:编译器不允许非bool值替换Bool,这个跟java等其他语言不同
  let i  = 1
  if i {
  }
  正确写法:
  if i==1 {}

六.元组Tuple

多个值组合成一个复合值,元组中的值可以是任何类型,并且可以是相互不同的类型。
场景:比如一个处理访问网页响应的函数,需要返回(Int,String)元组来描述成功失败的结果。

let http404Error = (404,"Not Found")  #类型是 (Int , String)
 #下标访问
 println("The status code is \(http404Error.0)")    #The status code is  404
 println("The status msg is \(http404Error.1)")    #The status msg is  Not Found

let  http200Status = (statusCode:200,description:"OK")
 #元素名称访问
 println("The status code is \(http200Status.statusCode)")    #The status code is  200
 println("The status msg is \(http200Status.description)")    #The status msg is OK

let (statusCode,statusMsg) = http404Error
 println("The status code is \(statusCode)")    #The status code is  404
 println("The status msg is \(statusMsg)")    #The status msg is  Not Found

let (justTheStatueCode, _) = http404Error    # 用( _ ) 来忽略不关心的部分
println("The status code is \(justTheStatueCode)")    #The status code is  404

七.Optional类型

 Optional 类型是swift独有的 ,表示一个基础类型可能有值也可能没有值,当他没有值的时候就是nil。

[Optional定义]
  定义一个Optional的值很容易,只需要在类型后面加上问号(?)就行了:
  var str : String?

 [显式拆包]
  Optional类型的值不能被直接使用,当需要用时要显式拆包,以表明我知道这个Optional是一定有值的,拆包用!:
  var str: String? = "Hello World!"
  str!     //Hello World!

  ps,Optional类型其实是一个枚举

[隐式拆包]
  除了显式拆包,Optional还提供了隐式拆包,通过在声明时的数据类型后面加一个感叹号(!)来实现:
  var str: String! = "Hello World!"
  str //Hello World!

[可选绑定Optional Binding]
  如下两个demo来讲述一下为甚么要Optiona Binding:

  看下面这个demo,当count != nil条件成真后接着在上下文中使用count,会被要求进行拆包 String(count!) ,
  var count: Int?
  count = 100
  if count != nil {
        "count is " + String(count!)    //count is 100
  } else {
        "nil"
  }

  为了避免在条件判断语句后执行一次或更多次的拆包,Swift引进了Optional Binding,我们就可以这样做:
  var count: Int?
  count = 100
  if let validCount = count {
        "count is " + String(validCount)    //count is 100
  } else {
        "nil"
  }

[Optional Chaining]
  暂时还没办法秒懂,mark一下,回头看
  http://blog.csdn.net/zhangao0086/article/details/38640209

八.断言

用于运行时校验对应条件是否等于true的判断。
let age = -3
assert(age>=0,"A person age can not less than zero")
只有断言的条件 age>=0的时候 才会继续执行,否则 age是负数 断言是false 那么就会触发断言 程序终止。

九.运算符

赋值  let b = 10
算数  +  -  *  /
取余  %
     这个符号要特殊说明一下,因为他与别的语言的取模不一样,他表示( a%b)  a中包括多少个b 并返回剩余的值,公式是
      a = (b * some multiplier) + remainder
      9 = (4 * 2) + 1   [9 % 4 = 1]
      -9=(4 * -2) + -1  [-9 % 4 = -1]    #可见 跟正负号没甚么关系
      8 = (2.5 * 3) + 0.5   #浮点数也一样的
自增  ++
自减  --
     var a = 0
     let  b = ++a     # b = 1 ; a = 1
     let  c = a++     # c = 1 ; a = 2
 一元减 -       理解成负号
 一元加 +       对操作数没甚么变化
 复合赋值运算     +=
 比较运算符       相等(a==b)    不相等(a!=b)    大于(a>b)   小于(a<b)  
                大于等于(a>=b) 小于等于(a<=b)   完全等(===) 不完全等(!==)
 三元条件        question ? answer1 : answer2
 范围运算符       闭合范围运算符  
                for index in 1...5 {}     #[1,5]
                半闭合范围运算符
                for index in 1..5 {}      #[1,5)
             
 逻辑运算符       逻辑非  !a
                逻辑与  a && 
                逻辑或  a || b

十.字符串和字符

[初始化空字符串]
   var emptyString = ""
   var anotherEmptyString = String()
   if emptyString.isEmpty {    #判断字符串为空
        emptyString += "给字符串加点内容"
   }
  
   string是拷贝类型,即 赋值,参数传递等 string值是生成一个副本的,不是原始的版本。与Cocoa的NSString不同。

[字符]
    let yenSign: Character = "¥"    #需要类型注释 和 字面量

    for character in "一个字符串" { 
          println(character)
     }

    #countElements统计字符数,全局函数
    let  unusualMsg = "slfjasfjslafjasfj"
    println("\(countElements(unusualMsg))")
    
[字符串的比较]
    字面值相等   if   str1 == str2 {  ......   }
    前缀相等     if str1.hasPrefix("Act 1")  { ........ }
    后缀相等     if str1.hasSuffix("Act 1")  { ........ }
    字符串大写  let strUpper = str.uppercaseString
    字符串小写  let strLower = str.lowercaseString
    
 [Unicode]
 关于Unicode 和 三种兼容unicode的字符串 自行查找资料进一步学习~
 三种兼容字符串是:UTF-8 编码单元集合    for c in str.utf8  { ... }
                UTF-16编码单元集合    for c in str.utf16  { ... }
                21位Unicode标量值的结合  for c in str.unicodeScalars  { ... }

十一.集合

集合有两类:
  数组:相同类型的有序列表
  字典:相同类型的无序列表

[数组定义]
var shoppingList: String[] = ["Eggs","Milk"]
var shoppingList: Array<String> = ["Eggs","Milk"]
var shoppingList = ["Eggs","Milk"]    #类型推断的存在,可以不用定义类型
var someInts = Int[] ()               #空数组
var someInts = []                     #空数组
#构造器,创建有若干默认值的数组
var threeDoubles = Double[] (count:3 ,repeatedValue: 0.0)
var anotherThreeDoubles = Array(count:3 ,repeatedValue: 2.5)
var sixDoubles = threeDoubles + anotherThreeDoubles

[数组操作]
shoppingList.count      #元素个数
shoppingList.isEmpty    #是否为空
shoppingList.append("flower") #追加元素
shoppingList += "Baking Powder"  #添加元素
shoppingList +=["str1","str2"]  #添加多个
var  firstItem = shoppingList[0]  #获取第一个元素
shoppingList[0] = "Egg2016"  #修改
shoppingList[4...6] = ["huhu","fafa"]   #修改下标4到6的三个元素为 huhu  , fafa 
shoppingList.insert("tomato",atIndex: 0)   #在指定位置插入
let mapleSyrup = shoppingList.removeAtIndex(0)   #删除index位置的元素
let apples = shoppingList.removeLast()    #删除最后一个元素,他会避免对count属性的查询,推荐使用这个 而不是 removeAtIndex(count-1)

[数组的遍历]
for  item  in  shoppingList { ... }

 #enumerate为每个元素返回一个元组
 for (index,value) in enumerate(shoppingList) {
       println("item \(index+1) : \(value) ")
 }

 [字典的定义]
 var airports: Dictionary<String,String> = ["a":"b","c":"d"]
 var airports = ["a":"b","c":"d"]   
 var airports = Dictionary<Int,String>()    #空字典
 airports = [:]  # 空字典字面量[:],前提是字典已经指定了类型

 [字典的操作]
 airports["e"] = "f"    #追加,如果key不存在
 airports["e"] = "g" #修改,如果key存在

 #updateValue更新某个key对应的值,返回字典值类型的可选
 if let oldValue = airports.updateValue("new value", forKey:"a") {
        println("\(oldValue)")
  }

 #删除键值对
 airports["APL"] = nil        #通过设置值为nil来删除键值对
 if let removedValue = airports.removeValueForKey("a"){
      println(" \(removedValue) ")
 }else {
      println("does not contain value for key a")
 }

[字典的遍历]
 for (airportCode,airportName) in airports {
 }

 for airportCode in airports.keys { .... }

 for airportName in airports.values { .... }

 let  airportCodes = Array(airports.keys)
 let  airportValues = Array(airports.values)

十二.流程控制

[for  index  in   集合]
        for  index  in   1...5      { ... }    
        for  _  in  1...n           { ... }     #下划线来占位,前提是 你需要用到集合中的元素值
        for  arrItem  in  oneArray { ... } 
        for  (code,msg) in  oneDic  { ... }
        for  character in "Hello"   { ... }

 =================================
 [for 循环]
  for  var index = 0;index < 3; ++index {  ....  }

 =================================
 [while 循环]
  while a < b {
        a++
  }

  do{
        a++
  } while  a<b
 =================================
 [if 语句]
  if condition  {
    ...
  }else if condition {
    ...
  }else {
    ...
 }
 =================================
 [switch 语句]
 swift不需要break去结束一个case执行,当然你也可以写,一个case命中执行后就结束了,不会跳转到下一个case。注意,每个case必须要有至少一条执行语句,不能是空的 会编译报错。

 let someCharacter: Character = "e"
 switch someCharacter {
   case "a","b","c":
        println("\(someCharacter)  is a  vowel")
   case "d","e","f":
        println("\(someCharacter)  is a  constant")
   default:
         println("default")
}

关于case支持的格式:
范围匹配   case 1...3: 
元组匹配   let somePoint = (1,1)
          switch somePoint {
            case (0,0):
                  println("(0,0) is at the origin")
            case (_,0):
                  println("用_来匹配任何可能的值")
            case (-2...2,-2...2):
                  println("用范围来匹配值")
            default:
                  println("默认命中")
           }

值绑定   switch somePoint {
            case (let x,0):
                  println("\(x) 被绑定了横坐标命中的值")
            case let(x,y):
                  println("获取绑定的值 ( \(x),\(y) ) ")
            default:
                  println("默认命中")
           }

 where分句检测额外条件
         switch somePoint {
            case let(x,y) where x==y:
                  println("绑定的值 ( \(x),\(y) ) on the line x == y ")
            default:
                  println("默认命中")
           }
 =================================
 [控制转移语句]
  continue    停止现在的工作继续下次迭代
  break       终止整个流程
  fallthrough switch执行一个case后会结束此次switch,用fall through会执行下一个case
              switch m {
                   case  "a":
                         XXXXXXX
                         fall through
                   case "b":
                         XXXXXXXX
                   default:
                        XXXXXXXX
              }
  标签语句
          gameLoop: ******
          continue gameLoop

十三.函数

[定义]
  #  ->指定函数的返回值
  func sayHello (personName: String) -> String {
        let greeting = "Hello," + personName + "!"
  }

 [参数]
  #多参,返回多个返回值
  func count(str: String,str2: String) -> (vowels: Int,consonants: Int,others: Int){
        return (vowels, consonants, others)
  }

  #外参,外部参数名可以在函数调用的时候对于参数有更加直观的认识。即该参数名对外部可见。
  import Foundation  
  func sayHello(username name:String)->String{  
        let result = "Hello," + name  
        return result    
  }  
  var nickname:String = "Jack"  
  println(sayHello(username: nickname))   #调用

  #同时可以使某个参数名同时是内部参数名和外部参数名:使用#号。
  func sayHello(#name:String)->String{   
      let result = "Hello," + name  
      return result  
  }  
  var nickname:String = "Jack"  
  println(sayHello(name: nickname)) 

  #默认行参值,swift会给你定义的默认行参提供一个自动外部名,比如joiner 会有一个外部名 joiner,跟加#类似
  fun join(str1: String,str2: String,joiner: String = "") -> String {
        return str1 + joiner + str2
  }
  join("hello","world")

  #

十四.闭包

十五.枚举

[定义]
  enum CompassPoint {
        case North
        case South
        case East
        case West
  } 
  
  var directionToHead = CompassPoint.West   #directionToHead就是CompassPoint类型的变量
  directionToHead = .East   #directionToHead的类型是已知的了,所以你可以忽略它的类型来给他赋值了

 [使用Switch语句来匹配枚举值]
   directionToHead = .South
   switch directionToHead {
      case .North:
            println("Lots of planets have a north")
      case .South:
            println("Watch out for penguins")
      case .East:
            println("Where the sun rises")
      case .West:
            println("Where the skies are blue")
    }

 [关联值]
  enum Barcode {
        case UPCA(Int, Int, Int)
        case QRCode(String)
  } 
  定义了一个叫做Barcode的枚举类型,它可以有一个UPCA成员,这个成员关联了一个包含三个整型数值的元组,同时这个枚举类型还有一个QRCode成员,关联了一个字符串。

  var productBarcode = Barcode.UPCA(8, 85909_51226, 3)    #可以用任意其中一个类型来生成一个新的
  productBarcode = .QRCode("ABCDEFGHIJKLMNOP")   #还可以被赋值为另一个类型,一旦定义了变量类型,再次赋值的时候不需要带类型Barcode

  switch productBarcode 
  {
    case .UPCA(let numberSystem, let identifier, let check):     #let可以提前 let .UPCA(numberSystem, identifier, check)
           println("UPC-A with value of \(numberSystem), \(identifier), \(check).") 
    case .QRCode(let productCode):    #let .QRCode(productCode)
           println("QR code with value of \(productCode).") 
  } 

[原始值]
  enum ASCIIControlCharacter: Character {
      case Tab = "\t"
      case LineFeed = "\n"
      case CarriageReturn = "\r"
  } 

  enum Planet: Int {
          case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
  } 
  let earthsOrder = Planet.Earth.toRaw()    #toRaw()方法来获取他的原始值,eathsOrder is 3 

  let possiblePlanet = Planet.fromRaw(7)    #fromRaw()方法来尝试通过一个原始值来寻找他所对应的枚举成员
  # possiblePlanet is of type Planet? and equals Planet.Uranus”
  # possiblePlanet是一个Planet?类型,可能会是nil,因此需要判断:
  let positionToFind = 9
  if let somePlanet = Planet.fromRaw(positionToFind) {  #可选绑定
       switch somePlanet {
            case .Earth:
                    println("ostly harmless")
           default:
                    println("Not a safe place for humans")
       }
   } else {
        println("There isn't a planet at position \(positionToFind)")   #Planet.fromRaw(positionToFind) 如果是 nil ,那么就会走到这
   }

十六.类和结构体

反馈一个问题:
 binary operator  ===  can not be applied
[swift语言详解]http://c.biancheng.net/cpp/html/2427.html 在数组的===比较这一块是错误的,恒等用在引用类型上,而数组是值类型,可以看下这个demo:
Paste_Image.png
[定义]
  struct Resolution {   #Resolution的结构体,用来描述一个显示器的像素分辨率
      var width = 0
      var heigth = 0
  }
  class VideoMode {     #VideoMode的类,用来描述一个视频显示器的特定模式
       var resolution = Resolution()
       var interlaced = false
       var frameRate = 0.0
       var name: String?
  } 

  let someResolution = Resolution()
  let vga = resolution(width:640, heigth: 480)
  let someVideoMode = VideoMode() 

  someVideoMode.resolution.width = 12880
  print(someResolution.width)

[属性]
 存储属性:
  struct FixedLengthRange {
      var firstValue: Int
      let length: Int
  }
  var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)

 延迟存储属性:
 延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用@lazy来标示一个延迟存储属性。
 必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。


class DataImporter {
    /*
    DataImporter 是一个将外部文件中的数据导入的类。
    这个类的初始化会消耗不少时间。
    */
    var fileName = "data.txt"
    // 这是提供数据导入功能
 }
class DataManager {
    @lazy var importer = DataImporter()
    var data = String[]()
    // 这是提供数据管理功能
}
let manager = DataManager()
manager.data += "Some data"
manager.data += "Some more data"
// DataImporter 实例的 importer 属性还没有被创建 

DataManager的一个功能是从文件导入数据,该功能由DataImporter类提供,DataImporter需要消耗不少时间完成初始化:因为它的实例在初始化时可能要打开文件,还要读取文件内容到内存。DataManager也可能不从文件中导入数据。所以当DataManager的实例被创建时,没必要创建一个DataImporter的实例,更明智的是当用到DataImporter的时候才去创建它。由于使用了@lazy,importer属性只有在第一次被访问的时候才被创建。比如访问它的属性fileName时:
  println(manager.importer.fileName)
  // DataImporter 实例的 importer 属性现在被创建了
  // 输出 "data.txt” 

计算属性:
类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。
struct Point {
    var x = 0.0, y = 0.0
}

struct Size {
    var width = 0.0, height = 0.0
}

struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
    get {
        let centerX = origin.x + (size.width / 2)
        let centerY = origin.y + (size.height / 2)
        return Point(x: centerX, y: centerY)
    }
    set(newCenter) {   #(newCenter) 可以省略不写,用默认的newValue
        origin.x = newCenter.x - (size.width / 2)    #origin.x = newValue.x - (size.width / 2)
        origin.y = newCenter.y - (size.height / 2)
    }
  }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
println("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 输出 "square.origin is now at (10.0, 10.0)”

属性监视器:
暂时不知道哪里用到,先不说了

类型属性:
略
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容