原創(chuàng)|使用教程|編輯:龔雪|2014-06-05 11:29:13.000|閱讀 1865 次
概述:本文為Swift編程語言中文教程第六部分,講解Swift的函數(shù),內(nèi)容包括:函數(shù)的聲明與調(diào)用、函數(shù)的參數(shù)和返回值、函數(shù)參數(shù)名等。Swift是蘋果公司在WWDC2014發(fā)布的一門編程語言,與Objective-C相比,對(duì)學(xué)習(xí)新手比較友好。慧都控件網(wǎng)根據(jù)官方教程以及網(wǎng)上中文資源整理了Swift編程語言中文教程,希望幫助想要學(xué)習(xí)Swift的朋友,由于技術(shù)有限,可能有不足的地方,希望大家指正。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關(guān)鏈接:
函數(shù)是執(zhí)行特定任務(wù)的代碼自包含塊。給定一個(gè)函數(shù)名稱標(biāo)識(shí), 當(dāng)執(zhí)行其任務(wù)時(shí)就可以用這個(gè)標(biāo)識(shí)來進(jìn)行”調(diào)用”。
Swift的統(tǒng)一的功能語法足夠靈活來表達(dá)任何東西,無論是甚至沒有參數(shù)名稱的簡(jiǎn)單的C風(fēng)格的函數(shù)表達(dá)式,還是需要為每個(gè)本地參數(shù)和外部參數(shù)設(shè)置復(fù)雜名稱的Objective-C語言風(fēng)格的函數(shù)。參數(shù)提供默認(rèn)值,以簡(jiǎn)化函數(shù)調(diào)用,并通過設(shè)置在輸入輸出參數(shù),在函數(shù)執(zhí)行完成時(shí)修改傳遞的變量。
Swift中的每個(gè)函數(shù)都有一個(gè)類型,包括函數(shù)的參數(shù)類型和返回類型。您可以方便的使用此類型像任何其他類型一樣,這使得它很容易將函數(shù)作為參數(shù)傳遞給其他函數(shù),甚至從函數(shù)中返回函數(shù)類型。函數(shù)也可以寫在其他函數(shù)中來封裝一個(gè)嵌套函數(shù)用以范圍內(nèi)有用的功能。
1、函數(shù)的聲明與調(diào)用
當(dāng)你定義一個(gè)函數(shù)時(shí),你可以為其定義一個(gè)或多個(gè)命名,定義類型值作為函數(shù)的輸入(稱為參數(shù)),當(dāng)該函數(shù)完成時(shí)將傳回輸出定義的類型(稱為作為它的返回類型)。
每一個(gè)函數(shù)都有一個(gè)函數(shù)名,用來描述了函數(shù)執(zhí)行的任務(wù)。要使用一個(gè)函數(shù)的功能時(shí),你通過使用它的名稱進(jìn)行“調(diào)用”,并通過它的輸入值(稱為參數(shù))來匹配函數(shù)的參數(shù)類型。一個(gè)函數(shù)的提供的參數(shù)必須始終以相同的順序來作為函數(shù)參數(shù)列表。
例如在下面的例子中被調(diào)用的函數(shù)greetingForPerson,像它描述的那樣 — 它需要一個(gè)人的名字作為輸入并返回一句問候給那個(gè)人。
func sayHello(personName: String) -> String {
let greeting = “Hello, ” + personName + “!”
return greeting
}
所有這些信息都匯總到函數(shù)的定義中,并以func關(guān)鍵字為前綴。您指定的函數(shù)的返回類型是以箭頭->(一個(gè)連字符后跟一個(gè)右尖括號(hào))以及隨后類型的名稱作為返回的。
該定義描述了函數(shù)的作用是什么,它期望接收什么,以及當(dāng)它完成返回的結(jié)果是什么。該定義很容易讓該函數(shù)可以讓你在代碼的其他地方以清晰、明確的方式來調(diào)用:
println(sayHello(“Anna”))
// prints “Hello, Anna!”
println(sayHello(“Brian”))
// prints “Hello, Brian!”
通過括號(hào)內(nèi)String類型參數(shù)值調(diào)用sayHello的函數(shù),如的sayHello(”Anna”)。由于該函數(shù)返回一個(gè)字符串值,sayHello的可以被包裹在一個(gè)println函數(shù)調(diào)用中來打印字符串,看看它的返回值,如上圖所示。
在sayHello的函數(shù)體開始定義了一個(gè)新的名為greeting的String常量,并將其設(shè)置加上personName個(gè)人姓名組成一句簡(jiǎn)單的問候消息。然后這個(gè)問候函數(shù)以關(guān)鍵字return來傳回。只要問候函數(shù)被調(diào)用時(shí),函數(shù)執(zhí)行完畢是就會(huì)返回問候語的當(dāng)前值。
你可以通過不同的輸入值多次調(diào)用sayHello的函數(shù)。上面的例子顯示了如果它以”Anna”為輸入值,以”Brian”為輸入值會(huì)發(fā)生什么。函數(shù)的返回在每種情況下都是量身定制的問候。
為了簡(jiǎn)化這個(gè)函數(shù)的主體,結(jié)合消息創(chuàng)建和return語句用一行來表示:
func sayHello(personName: String) -> String {
return “Hello again, ” + personName + “!”
}
println(sayHello(“Anna”))
// prints “Hello again, Anna!”
2、函數(shù)的參數(shù)和返回值
在swift中函數(shù)的參數(shù)和返回值是非常具有靈活性的。你可以定義任何東西無論是一個(gè)簡(jiǎn)單的僅僅有一個(gè)未命名的參數(shù)的函數(shù)還是那種具有豐富的參數(shù)名稱和不同的參數(shù)選項(xiàng)的復(fù)雜函數(shù)。
多輸入?yún)?shù)
函數(shù)可以有多個(gè)輸入?yún)?shù),把他們寫到函數(shù)的括號(hào)內(nèi),并用逗號(hào)加以分隔。下面這個(gè)函數(shù)設(shè)置了一個(gè)開始和結(jié)束索引的一個(gè)半開區(qū)間,用來計(jì)算在范圍內(nèi)有多少元素包含:
func halfOpenRangeLength(start: Int, end: Int) -> Int {
return end – start
}
println(halfOpenRangeLength(1, 10))
// prints “9″
無參函數(shù)
函數(shù)并沒有要求一定要定義的輸入?yún)?shù)。下面就一個(gè)沒有輸入?yún)?shù)的函數(shù),任何時(shí)候調(diào)用時(shí)它總是返回相同的字符串消息:
func sayHelloWorld() -> String {
return “hello, world”
}
println(sayHelloWorld())
// prints “hello, world”
該函數(shù)的定義在函數(shù)的名稱后還需要括號(hào),即使它不帶任何參數(shù)。當(dāng)函數(shù)被調(diào)用時(shí)函數(shù)名稱也要跟著一對(duì)空括號(hào)。
沒有返回值的函數(shù)
函數(shù)也不需要定義一個(gè)返回類型。這里有一個(gè)版本的sayHello的函數(shù),稱為waveGoodbye,它會(huì)輸出自己的字符串值而不是函數(shù)返回:
func sayGoodbye(personName: String) {
println(“Goodbye, (personName)!”)
}
sayGoodbye(“Dave”)
// prints “Goodbye, Dave!”
因?yàn)樗⒉恍枰祷匾粋€(gè)值,該函數(shù)的定義不包括返回箭頭( – >)和返回類型。
提示
嚴(yán)格地說,sayGoodbye功能確實(shí)還返回一個(gè)值,即使沒有返回值定義。函數(shù)沒有定義返回類型但返
回了一個(gè)void返回類型的特殊值。它是一個(gè)簡(jiǎn)直是空的元組,實(shí)際上零個(gè)元素的元組,可以寫為()。
當(dāng)一個(gè)函數(shù)調(diào)用時(shí)它的返回值可以忽略不計(jì):
func printAndCount(stringToPrint: String) -> Int {
println(stringToPrint)
return countElements(stringToPrint)
}
func printWithoutCounting(stringToPrint: String) {
printAndCount(stringToPrint)
}
printAndCount(“hello, world”)
// prints “hello, world” and returns a value of 12
printWithoutCounting(“hello, world”)
// prints “hello, world” but does not return a value
第一個(gè)函數(shù)printAndCount,打印了一個(gè)字符串,然后并以Int類型返回它的字符數(shù)。第二個(gè)函數(shù)printWithoutCounting,調(diào)用的第一個(gè)函數(shù),但忽略它的返回值。當(dāng)?shù)诙瘮?shù)被調(diào)用時(shí),字符串消息由第一函數(shù)打印了回來,去沒有使用其返回值。
提示
返回值可以忽略不計(jì),但對(duì)一個(gè)函數(shù)來說,它的返回值即便不使用還是一定會(huì)返回的。在函數(shù)體底部
返回時(shí)與定義的返回類型的函數(shù)不能相容時(shí),如果試圖這樣做將導(dǎo)致一個(gè)編譯時(shí)錯(cuò)誤。
多返回值函數(shù)
你可以使用一個(gè)元組類型作為函數(shù)的返回類型返回一個(gè)有多個(gè)值組成的一個(gè)復(fù)合作為返回值。
下面的例子定義了一個(gè)名為count函數(shù),用它計(jì)來算字符串中基于標(biāo)準(zhǔn)的美式英語中設(shè)定使用的元音、輔音以及字符的數(shù)量:
func count(string: String) -> (vowels: Int, consonants: Int, others: Int) {
var vowels = 0, consonants = 0, others = 0
for character in string {
switch String(character).lowercaseString {
case “a”, “e”, “i”, “o”, “u”:
++vowels
case “b”, “c”, “d”, “f”, “g”, “h”, “j”, “k”, “l”, “m”,
“n”, “p”, “q”, “r”, “s”, “t”, “v”, “w”, “x”, “y”, “z”:
++consonants
default:
++others
}
}
return (vowels, consonants, others)
}
您可以使用此計(jì)數(shù)函數(shù)來對(duì)任意字符串進(jìn)行字符計(jì)數(shù),并檢索統(tǒng)計(jì)總數(shù)的元組三個(gè)指定Int值:
let total = count(“some arbitrary string!”)
println(“(total.vowels) vowels and (total.consonants) consonants”)
// prints “6 vowels and 13 consonants”
需要注意的是在這一點(diǎn)上元組的成員不需要被命名在該該函數(shù)返回的元組中,因為他們的名字已經(jīng)被指定為函數(shù)的返回類型的一部分。
3、函數(shù)參數(shù)名
所有上面的函數(shù)都為參數(shù)定義了參數(shù)名稱:
func someFunction(parameterName: Int) {
// function body goes here, and can use parameterName
// to refer to the argument value for that parameter
}
然而,這些參數(shù)名的僅能在函數(shù)本身的主體內(nèi)使用,在調(diào)用函數(shù)時(shí),不能使用。這些類型的參數(shù)名稱被稱為本地的參數(shù),因?yàn)樗鼈冎贿m用于函數(shù)體中使用。
外部參數(shù)名
有時(shí)當(dāng)你調(diào)用一個(gè)函數(shù)將每個(gè)參數(shù)進(jìn)行命名是非常有用的,以表明你傳遞給函數(shù)的每個(gè)參數(shù)的目的。
如果你希望用戶函數(shù)調(diào)用你的函數(shù)時(shí)提供參數(shù)名稱,除了設(shè)置本地地的參數(shù)名稱,也要為每個(gè)參數(shù)定義外部參數(shù)名稱。你寫一個(gè)外部參數(shù)名稱在它所支持的本地參數(shù)名稱之前,之間用一個(gè)空格來分隔:
func someFunction(externalParameterName localParameterName: Int) {
// function body goes here, and can use localParameterName
// to refer to the argument value for that parameter
}
注意
如果您為參數(shù)提供一個(gè)外部參數(shù)名稱,調(diào)用該函數(shù)時(shí)外部名稱必須始終被使用。
作為一個(gè)例子,考慮下面的函數(shù),它通過插入他們之間的第三個(gè)”joiner”字符串來連接兩個(gè)字符串:
func join(s1: String, s2: String, joiner: String) -> String {
return s1 + joiner + s2
}
當(dāng)你調(diào)用這個(gè)函數(shù),你傳遞給函數(shù)的三個(gè)字符串的目的就不是很清楚了:
join(“hello”, “world”, “, “)
// returns “hello, world”
為了使這些字符串值的目的更為清晰,為每個(gè)join函數(shù)參數(shù)定義外部參數(shù)名稱:
func join(string s1: String, toString s2: String, withJoiner joiner: String)
-> String {
return s1 + joiner + s2
}
在這個(gè)版本的join函數(shù)中,第一個(gè)參數(shù)有一個(gè)外部名稱string和一個(gè)本地名稱s1;第二個(gè)參數(shù)有一個(gè)外部名稱toString和一個(gè)本地名稱s2;第三個(gè)參數(shù)有一個(gè)外部名稱withJoiner和一個(gè)本地名稱joiner。
現(xiàn)在,您可以使用這些外部參數(shù)名稱調(diào)用清楚明確的調(diào)用該函數(shù):
join(string: “hello”, toString: “world”, withJoiner: “, “)
// returns “hello, world”
使用外部參數(shù)名稱使join函數(shù)的第二個(gè)版本功能更富有表現(xiàn)力,用戶習(xí)慣使用sentence-like的方式,同時(shí)還提供了一個(gè)可讀的、意圖明確的函數(shù)體。
注意
考慮到使用外部參數(shù)名稱的初衷就是為了在別人第一次閱讀你的代碼時(shí)并不知道你函數(shù)參數(shù)的目的是什么。
但當(dāng)函數(shù)調(diào)用時(shí)如果每個(gè)參數(shù)的目的是明確的和毫不含糊的,你并不需要指定外部參數(shù)名稱。
外部參數(shù)名稱速記
如果你想為一個(gè)函數(shù)參數(shù)提供一個(gè)外部參數(shù)名,然而本地參數(shù)名已經(jīng)使用了一個(gè)合適的名稱了,你不需要為該參數(shù)寫相同的兩次名稱。取而代之的是,寫一次名字,并用一個(gè)hash符號(hào)(#)作為名稱的前綴。這告訴Swift使用該名稱同時(shí)作為本地參數(shù)名稱和外部參數(shù)名稱。
這個(gè)例子定義了一個(gè)名為containsCharacter的函數(shù),定義了兩個(gè)參數(shù)的外部參數(shù)名稱并通過放置一個(gè)散列標(biāo)志在他們本地參數(shù)名稱之前:
func containsCharacter(#string: String, #characterToFind: Character) -> Bool {
for character in string {
if character == characterToFind {
return true
}
}
return false
}
這個(gè)函數(shù)選擇的參數(shù)名稱清晰的、函數(shù)體極具可讀性,使的該函數(shù)被調(diào)用時(shí)沒有歧義:
let containsAVee = containsCharacter(string: “aardvark”, characterToFind: “v”)
// containsAVee equals true, because “aardvark” contains a “v”
參數(shù)的默認(rèn)值
可以為任何參數(shù)設(shè)定默認(rèn)值來作為函數(shù)的定義的一部分。如果默認(rèn)值已經(jīng)定義,調(diào)用函數(shù)時(shí)就可以省略該參數(shù)的傳值。
注意
將使用默認(rèn)值的參數(shù)放在函數(shù)的參數(shù)列表的末尾。這確保了所有調(diào)用函數(shù)的非默認(rèn)參數(shù)使用相同的順
序,并明確地表示在每種情況下相同的函數(shù)調(diào)用。
這里有一個(gè)版本,是早期的join函數(shù),并為參數(shù)joiner設(shè)置了默認(rèn)值:
func join(string s1: String, toString s2: String,
withJoiner joiner: String = ” “) -> String {
return s1 + joiner + s2
}
如果在join函數(shù)被調(diào)用時(shí)提供給joiner一個(gè)字符串值,該字符串是用來連接兩個(gè)字符串,就跟以前一樣:
join(string: “hello”, toString: “world”, withJoiner: “-”)
// returns “hello-world”
但是,如果當(dāng)函數(shù)被調(diào)用時(shí)提供了joiner的沒有值,就會(huì)使用單個(gè)空格(” “)的默認(rèn)值:
join(string: “hello”, toString: “world”)
// returns “hello world”
有默認(rèn)值的外部名稱參數(shù)
在大多數(shù)情況下,為所有參數(shù)提供一個(gè)外部帶有默認(rèn)值的參數(shù)的名稱是非常有用的(因此要求)。這將確如果當(dāng)函數(shù)被調(diào)用時(shí)提供的值時(shí)參數(shù)必須具有明確的目的。
為了使這個(gè)過程更容易,當(dāng)你自己沒有提供外部名稱時(shí),Swift自動(dòng)為所有參數(shù)定義了缺省的參數(shù)外部名稱。自動(dòng)外部名稱與本地名稱相同,就好像你在你的代碼中的本地名稱之前寫了一個(gè)hash符號(hào)。
這里有一個(gè)早期join函數(shù)版本,它不為任何參數(shù)提供的外部名稱,但仍然提供了joiner參數(shù)的默認(rèn)值:
func join(s1: String, s2: String, joiner: String = ” “) -> String {
return s1 + joiner + s2
}
在這種情況下,Swift自動(dòng)為一個(gè)具有默認(rèn)值的參數(shù)提供了外部參數(shù)名稱。調(diào)用函數(shù)時(shí),為使得參數(shù)的目的明確、毫不含糊,因此必須提供外部名稱:
join(“hello”, “world”, joiner: “-”)
// returns “hello-world”
注意
你可以通過編寫一個(gè)下劃線(_)有選擇進(jìn)行這種行為,而不是一個(gè)明確的定義外部參數(shù)名稱。然
而,在適當(dāng)情況下有默認(rèn)值的外部名稱參數(shù)總是優(yōu)先被使用。
可變參數(shù)
一個(gè)可變參數(shù)的參數(shù)接受零個(gè)或多個(gè)指定類型的值。當(dāng)函數(shù)被調(diào)用時(shí),您可以使用一個(gè)可變參數(shù)的參數(shù)來指定該參數(shù)可以傳遞不同數(shù)量的輸入值。寫可變參數(shù)的參數(shù)時(shí),需要參數(shù)的類型名稱后加上點(diǎn)字符(…)。
傳遞一個(gè)可變參數(shù)的參數(shù)的值時(shí),函數(shù)體中是以提供適當(dāng)類型的數(shù)組的形式存在。例如,一個(gè)可變參數(shù)的名稱為numbers和類型為Double…在函數(shù)體內(nèi)就作為名為numbers類型為Double[]的常量數(shù)組。
下面的示例計(jì)算任意長度的數(shù)字的算術(shù)平均值(也稱為平均):
func arithmeticMean(numbers: Double…) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8, 19)
// returns 10.0, which is the arithmetic mean of these three numbers
注意
函數(shù)可以最多有一個(gè)可變參數(shù)的參數(shù),而且它必須出現(xiàn)在參數(shù)列表的最后以避免多參數(shù)函
數(shù)調(diào)用時(shí)出現(xiàn)歧義。
如果函數(shù)有一個(gè)或多個(gè)參數(shù)使用默認(rèn)值,并且還具有可變參數(shù),將可變參數(shù)放在列表的
最末尾的所有默認(rèn)值的參數(shù)之后。
常量參數(shù)和變量參數(shù)
函數(shù)參數(shù)的默認(rèn)值都是常量。試圖改變一個(gè)函數(shù)參數(shù)的值會(huì)讓這個(gè)函數(shù)體內(nèi)部產(chǎn)生一個(gè)編譯時(shí)錯(cuò)誤。這意味著您不能錯(cuò)誤地改變參數(shù)的值。
但是,有時(shí)函數(shù)有一個(gè)參數(shù)的值的變量副本是非常有用的。您可以通過指定一個(gè)或多個(gè)參數(shù)作為變量參數(shù),而不是避免在函數(shù)內(nèi)部為自己定義一個(gè)新的變量。變量參數(shù)可以是變量而不是常量,并給函數(shù)中新修改的參數(shù)的值的提供一個(gè)副本。
在參數(shù)名稱前用關(guān)鍵字var定義變量參數(shù):
func alignRight(var string: String, count: Int, pad: Character) -> String {
let amountToPad = count – countElements(string)
for _ in 1…amountToPad {
string = pad + string
}
return string
}
let originalString = “hello”
let paddedString = alignRight(originalString, 10, “-”)
// paddedString is equal to “—–hello”
// originalString is still equal to “hello”
這個(gè)例子定義了一個(gè)新函數(shù)叫做alignRight,它對(duì)準(zhǔn)一個(gè)輸入字符串,以一個(gè)較長的輸出字符串。在左側(cè)的空間中填充規(guī)定的字符。在該示例中,字符串”hello”被轉(zhuǎn)換為字符串”—–hello”。
該alignRight函數(shù)把輸入?yún)?shù)的字符串定義成了一個(gè)變量參數(shù)。這意味著字符串現(xiàn)在可以作為一個(gè)局部變量,用傳入的字符串值初始化,并且可以在函數(shù)體中進(jìn)行相應(yīng)操作。
函數(shù)首先找出有多少字符需要被添加到左邊讓字符串以右對(duì)齊在整個(gè)字符串中。這個(gè)值存儲(chǔ)在本地常量amountToPad中。該函數(shù)然后將填充字符的amountToPad個(gè)字符拷貝到現(xiàn)有的字符串的左邊,并返回結(jié)果。整個(gè)過程使用字符串變量參數(shù)進(jìn)行字符串操作。
注意
一個(gè)變量參數(shù)的變化沒有超出了每個(gè)調(diào)用函數(shù),所以對(duì)外部函數(shù)體是不可見的。變量參數(shù)只能存在于函數(shù)調(diào)用
的生命周期里。
輸入-輸出參數(shù)
可變參數(shù),如上所述,只能在函數(shù)本身內(nèi)改變。如果你想有一個(gè)函數(shù)來修改參數(shù)的值,并且想讓這些變化要堅(jiān)持在函數(shù)調(diào)用結(jié)束后,你就可以定義輸入-輸出參數(shù)來代替。
通過在其參數(shù)定義的開始添加inout關(guān)鍵字寫用來標(biāo)明輸入-輸出參數(shù)。一個(gè)在輸入-輸出參數(shù)都有一個(gè)傳遞給函數(shù)的值,由函數(shù)修改后,并從函數(shù)返回來替換原來的值。
4、函數(shù)類型
//待翻譯
使用函數(shù)類型
//待翻譯
函數(shù)類型的參數(shù)
//待翻譯
函數(shù)類型的返回值
//待翻譯
println(“Counting to zero:”)
// Counting to zero:
while currentValue != 0 {
println(“(currentValue)… “)
currentValue = moveNearerToZero(currentValue)
}
println(“zero!”)
// 3…
// 2…
// 1…
// zero!
5、嵌套函數(shù)
迄今為止所有你在本章中遇到函數(shù)都是全局函數(shù),在全局范圍內(nèi)定義。其實(shí)你還可以在其他函數(shù)中定義函數(shù),被稱為嵌套函數(shù)。
嵌套函數(shù)默認(rèn)對(duì)外界是隱藏的,但仍然可以調(diào)用和使用其內(nèi)部的函數(shù)。內(nèi)部函數(shù)也可以返回一個(gè)嵌套函數(shù),允許在嵌套函數(shù)內(nèi)的另一個(gè)范圍內(nèi)使用。
你可以重寫上面的chooseStepFunction例子使用并返回嵌套函數(shù):
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input – 1 }
return backwards ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
println(“(currentValue)… “)
currentValue = moveNearerToZero(currentValue)
}
println(“zero!”)
// -4…
// -3…
// -2…
// -1…
// zero!
本文資源來自互聯(lián)網(wǎng),由本網(wǎng)整理編輯,供大家學(xué)習(xí)參考。因?yàn)榧夹g(shù)有限,可能會(huì)有不足及錯(cuò)誤,請(qǐng)大家指正。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@fc6vip.cn