以前在用C#開發(fā)程序的時侯,只要用到數(shù)組,必然離不開泛型季二。再配合集合的擴(kuò)展方法和LINQ檩咱,對于集合數(shù)據(jù)的操作真是得心應(yīng)手,非常爽快胯舷。后來我轉(zhuǎn)到iOS開發(fā)刻蚯,學(xué)會了Objective語言。發(fā)現(xiàn)在Objective-C里,一般最常用的數(shù)組容器就是NSArray和字典NSDictionary了桑嘶〈缎冢可惜這兩者都不支持泛型。數(shù)據(jù)都是以NSObject的類型添加進(jìn)來不翩,理論上可以保存保存任何的引用類型兵扬,這就需要讓開發(fā)者來確保所有添加的數(shù)據(jù)類型的一至性。同樣的口蝠,當(dāng)從數(shù)組容器里取值時器钟,需要將其轉(zhuǎn)化成對應(yīng)的Model。有時侯還需要進(jìn)行類型判斷妙蔗。這樣的操作不僅增加了代碼的復(fù)雜性傲霸,也很容易出錯。而Swift作為一門后起之秀眉反,必然添加了對泛型的支持昙啄。彌補(bǔ)了Objective-C對數(shù)組操作的安全性,復(fù)雜性等的不足寸五,而本篇文章向大家介紹了如果來定義一個泛型類梳凛,通過它來作為一種可以永久保存任意數(shù)據(jù)類型的數(shù)據(jù)容器,來實(shí)現(xiàn)iOS的數(shù)據(jù)持久化梳杏。
什么是泛型
為了更直觀一點(diǎn),首先讓我們來看看不支持泛型的Objective-C語言是怎么利用數(shù)據(jù)容器的
@interface demo:NSObject//自定義對象
@property (nonatomic,copy) NSString* demoString;
@end
@implementation demo
@end
NSMutableArray* arr = [NSMutableArray new];
[arr addObject:[NSNumber numberWithBool:YES]]; //添加Bool類型
[arr addObject:@"111"]; //可以字符串
demo* dm = [demo new];
dm.demoString = @"String";
[arr addObject:dm]; //添加自定義對象
NSLog(@"%@",arr);
BOOL a = [arr[0] boolValue]; //需要轉(zhuǎn)成Bool
NSString* b = arr[1] ; //直接將id類型賦到NSString類型
demo* dm1 = arr[2]; //直接將id賦值給demo類型
NSLog(@"a:%hhd b:%@.dm:%@",a,b,dm1); //可以正確地打印出來
NSString* dm2 = arr[2]; //也可以將本身是demo類型的賦值到String
NSLog(@"%@",dm2); //不會報(bào)錯.運(yùn)行時dm2本身還是demo類型
dm2.length; //調(diào)用length方法就會出錯,
打印結(jié)果:
2016-03-31 15:29:28.437 DemoObjc[1108:32945] (
1,
111,
"<demo: 0x1001025f0>"
)
2016-03-31 15:29:28.438 DemoObjc[1108:32945] a:1 b:111.dm:<demo: 0x1001025f0>
2016-03-31 15:29:28.438 DemoObjc[1108:32945] <demo: 0x1001025f0>//雖然在代碼階段是NSString類型韧拒,但是運(yùn)行時是demo類型
2016-03-31 15:29:28.438 DemoObjc[1108:32945] -[demo length]: unrecognized selector sent to instance 0x1001025f0
2016-03-31 15:29:28.439 DemoObjc[1108:32945] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[demo length]: unrecognized selector sent to instance 0x1001025f0'
)
從上面的示例代碼可以很清楚地看到,因?yàn)镺bjective-C不支持泛型十性,所以編譯器沒有做任何限制叛溢,可以在NSMutableArray里添加任何引用類型的數(shù)據(jù),這些數(shù)據(jù)都以id類型保存劲适。在取值時也一樣楷掉,默認(rèn)取出來的數(shù)據(jù)都是id類型,需要將其轉(zhuǎn)化成原先保存的類型才行霞势。這些操作都是需要開發(fā)者來確保正確性烹植。如果不小心寫錯了斑鸦,編譯器也不會給出任何提示或者警告,只有在運(yùn)行程序時才能觸發(fā)錯誤草雕。所以很容易出現(xiàn)隱藏的Bug鄙才,同時類型轉(zhuǎn)換也增加了代碼的復(fù)雜性。
而泛型的出現(xiàn)剛好完全解決了上面的問題:
var arrInt = [Int]() //定義了一個數(shù)據(jù)類型是Int類型的數(shù)組,其實(shí)就是一個只能添加Int類型數(shù)據(jù)的NSMutableArray促绵。
arrInt.append(1) //添加1
arrInt.append(2) //添加2
arrInt.append("123") //添加字符串“123”
//編譯器報(bào)錯,Cannot convert value of type 'String' to expected argument type 'Int'
//不能將String轉(zhuǎn)換成Int
print(arrInt[0].dynamicType) //取第一個值 嘴纺,打印出類型是Int
let str:String = arrInt[1] //將第二個值賦值給String
//編譯器報(bào)錯败晴, Cannot convert value of type 'Int' to specified type 'String'
//不能將轉(zhuǎn)Int換成String
從上面的代碼很容易看出,對于數(shù)據(jù)類型是Int的數(shù)組栽渴,只能添加Int類型的數(shù)據(jù)尖坤,其他的數(shù)據(jù)類型編譯器都會報(bào)錯。取值也一樣闲擦,取出來的數(shù)直接就是Int類型了慢味,不需要再轉(zhuǎn)型了。
而且比較方便的是Swift的數(shù)據(jù)容器是是可以直接保存值類型的墅冷,不需要像Objective-C一樣需要將其轉(zhuǎn)成NSNumber了纯路。同樣,你也可以聲明一個類型為String類型的數(shù)組([String])寞忿,
或者自定義類的的泛型數(shù)組驰唬,編譯器都會幫你在編碼時告訴你正確的數(shù)據(jù)類型,防止開發(fā)者添加錯誤的數(shù)據(jù)類型腔彰。
使用泛型的優(yōu)點(diǎn)
使用泛型的優(yōu)點(diǎn)有很多:
- 泛型提供了一個強(qiáng)類型的編程模型
- 編譯時的類型檢查減少了運(yùn)行時發(fā)生數(shù)據(jù)類型轉(zhuǎn)換異常的幾率
- 簡化了代碼叫编,緩解了代碼膨脹。
- 性能得到了提升霹抛,不需要在運(yùn)行時再做類型檢查搓逾。
- 代碼的可讀性更好,并且有更好的代碼智能提示杯拐。
其實(shí)在最新的XCode 7.X中霞篡,蘋果也悄悄地加入了Objective-C語言的弱泛型支持,見下面代碼。
NSMutableArray<NSString *>* arrString = [NSMutableArray new]; //可以在NSMutableArray后面加上數(shù)據(jù)類型藕施,這樣就聲明了一個泛型的NSMutableArray
[arrString addObject:[NSNumber numberWithUnsignedInteger:1]]; //這里編譯器會警告你添加了錯誤的數(shù)據(jù)類型寇损,但是不會強(qiáng)制報(bào)錯。
可以在聲明NSMutableArray時添加一個弱泛型約束裳食,之所以是弱泛型矛市,是因?yàn)榫幾g器會幫你檢查數(shù)據(jù)類型是否正確,如果不正確會有一個警告诲祸,但是不會強(qiáng)制報(bào)錯浊吏,代碼還是可以編譯過的而昨。
寫代碼時XCode會自動提示它你應(yīng)該添加什么類型的數(shù)據(jù)
取值也會告訴你里面保存了什么類型的數(shù)據(jù)
如果你把錯誤的數(shù)據(jù)類型存進(jìn)去,編譯器會警告找田,但不是強(qiáng)制報(bào)錯歌憨。
iOS數(shù)據(jù)持久化方案的問題
數(shù)據(jù)存儲在APP開發(fā)中起著至關(guān)重要的作用。相信各位開發(fā)者在開發(fā)過程中都有碰到如下情況:
- 需要一些全局變量,來記錄APP的一些設(shè)置或者是頻繁變動的數(shù)據(jù)
- 頁面之間或者各種View之間的傳值,
- 需要臨時緩存一些數(shù)據(jù)墩衙。
這些數(shù)據(jù)存儲的處理在開發(fā)過程極為常見务嫡,而且有一個共同點(diǎn)就是處理的各種數(shù)據(jù)類型完全不一樣,有時是各種數(shù)字(NSNumber),也有很常用的字符串漆改,當(dāng)然各種數(shù)組或者字典也是不可少的心铃。
所以由此帶來的一個問題就是這些數(shù)據(jù)需要以什么的格式保存,保存后取出來又要如何轉(zhuǎn)化成原先的數(shù)據(jù)類型挫剑,這些都是要手寫代碼去處理去扣。前面已經(jīng)說過,泛型正好是解決此類問題而生樊破。但在這里并不是使用泛型數(shù)據(jù)容器愉棱,而是使用泛型類。那么怎么使用泛型類來寫一個通用的數(shù)據(jù)存儲框架哲戚,可以解決以上問題呢奔滑?
首先iOS的數(shù)據(jù)存儲離不開iOS存儲本身的機(jī)制,也就是那如下幾種:
- Plist
- 歸檔&NSUserDefault
- SQLite3
- CoreData
關(guān)于這些數(shù)據(jù)持久化的介紹和使用方式網(wǎng)絡(luò)上有很多的文章講解惫恼,在這里我就不詳述了档押。而且目前市面上還有不少對這些數(shù)據(jù)持久化API的封裝開源庫,比如說著名的FMDB祈纯,但是都沒有完全解決上面的問題令宿。我們需要一個輕量級,可以在代碼文件的任何位置讀寫腕窥,支持緩存,臨時存儲(APP退出后數(shù)據(jù)丟失)和數(shù)據(jù)發(fā)生變化時的監(jiān)視粒没,同時不需要做數(shù)據(jù)類型的轉(zhuǎn)換且可以保存任何類型數(shù)據(jù)的數(shù)據(jù)存儲方案。而Swift泛型的出現(xiàn)簇爆,使得這種存儲方案成為可能癞松。
使用泛型類來存儲數(shù)據(jù)
和泛型數(shù)據(jù)容器相比,自定義泛型類用得并不多入蛆∠烊兀可能許多初級開發(fā)者不好理解泛型類,更不明白泛型類有什么作用哨毁。關(guān)于這個枫甲,可以去參考Swift的Array
或者是NSDictionry
的定義,并且練下手會更好一點(diǎn)。下面我們來定義一個泛型類
public class GrandStore<T> { //在類名后面加個<T>表示這是一個泛型類
private var name:String! //這個變量極為重要想幻,相當(dāng)于是一個Key粱栖,表示這個存儲的名稱。
private var value:T? //私有內(nèi)部存儲值脏毯,是個泛型字段
private var defaultValue:T? //默認(rèn)值闹究,也是個泛型字段
private var hasValue:Bool = false //Bool類型,用來判斷有沒有值
private var timeout:Int = 0 //緩存時間食店,如果你設(shè)定一個帶緩存的存儲數(shù)據(jù)渣淤,需要設(shè)定這個值
private var storeLevel:Int = 0 //存儲等級,用來判斷保存在什么地方
private var isTemp = false //如果設(shè)為true 吉嫩,那么只是放到內(nèi)存里臨時保存
private var timeoutDate:NSDate? //過期的時間砂代,用于帶緩存的存儲
}
上面就是這個泛型存儲類的全部私有字段。各有什么作用注釋里都有說明率挣,下面上它的兩個構(gòu)造器
public init(name:String,defaultValue:T) { //最常用的構(gòu)造器,適用于任何需要永久保存的數(shù)據(jù)
self.name = name;
self.defaultValue = defaultValue;
storeLevel = self.getStoreLevel() //獲取存儲級別露戒,
}
public init(name:String,defaultValue:T,timeout:Int) { //如果你要保存帶緩存的數(shù)據(jù)椒功,需要調(diào)用這個構(gòu)造器。timeout表示你需要緩存的時間智什,單位是秒
self.name = name;
self.defaultValue = defaultValue;
self.timeout = timeout
if self.timeout > 0{
timeoutDate = NSDate(timeIntervalSinceNow: Double(self.timeout))
}
else{ //如果timeout<=0話动漾,那么就是臨時存儲,只保存的內(nèi)存里面荠锭,APP退出后丟失
isTemp = true
}
storeLevel = self.getStoreLevel() //獲取存儲級別旱眯,
}
上面是泛型類的兩個構(gòu)造器彭沼。因?yàn)槠鋮?shù)是T類型硼补,也就是泛型求泰,那么就可以往里在傳任何類型蔫敲。同時這是一個默認(rèn)值脖旱,如果你沒有設(shè)定值的話初茶,那也是可以取出值的蝇率,就是這個默認(rèn)值考润。
后面的構(gòu)造器就是帶緩存有的了拥坛。如果你傳的緩存時間大于0蓬蝶,那么這個時間(秒為單位)就是緩存的時間,如果小于等于0猜惋,那就這是一個臨時存儲丸氛。它不會寫到硬盤里面,只保存在內(nèi)存里著摔。
下面上這個泛型類最核心的屬性缓窜,
public var Value:T?
{
get{
if isExpire{ //判斷有沒有過期
self.clear() //過期了就清空,再將是否有值設(shè)成false
hasValue = false
}
if!hasValue //如果沒有值,也就是說過期了或者內(nèi)存中沒有
{
if isTemp{ //如果是臨時保存的雹洗,直接將其設(shè)成默認(rèn)值
if self.value == nil{
self.value = defaultValue
}
}
else{
if !store.hasValue(name){ //判斷存儲倉庫里有沒有保存
self.value = defaultValue //如果沒有保存香罐,就設(shè)成默認(rèn)值再保存,
store.setValue(self.value)forkey(name)
}
else{
self.value = store.setValueForKey(name) as? T//有的話直接取出來
}
}
hasValue = true //將是否有值設(shè)成true
}
return self.value //返回取出來的值
}
set{
self.value = newValue
if !isTemp{
store.setValue(self.value)forkey(name) //設(shè)值就比較簡單了时肿,直接保存就行了庇茫、
}
hasValue = true
}
}
這里我用了偽代碼,同時省略了一些功能螃成。因?yàn)樵a比較長也有點(diǎn)復(fù)雜旦签,這里有幾個關(guān)鍵點(diǎn)要說明下
- 最上面的isExpire是個計(jì)算屬性,用來判斷保存的數(shù)據(jù)有沒有過期寸宏,如果是永久存在的宁炫,就直接返回false,如果不是氮凝,那么根據(jù)timeoutDate來判斷有沒有過期
private var isExpire:Bool{
get{
if timeoutDate == nil{ //對于永久保存有羔巢,直接返回false
return false
}
else{
return NSDate().compare(timeoutDate!) == NSComparisonResult.OrderedDescending //對于有緩存的,根據(jù)timeoutDate判斷有沒有過期
}
}
}
如果已經(jīng)過期罩阵,那么需要清空數(shù)據(jù)竿秆,再將hasValue設(shè)成false。
- 如果hasValue是false稿壁,也就是說內(nèi)存中不存在該值幽钢,那么就需要到存儲倉庫去看了。這里面我省略了一些代碼傅是》搜啵總體思路如下:
先判斷存儲倉庫有沒有,如果沒有喧笔,就直接設(shè)置成默認(rèn)值帽驯,再保存到存儲倉庫里。
如果存在书闸,就從存儲倉庫里取出界拦,最后再將hasValue設(shè)置成true。
在這里存儲倉庫是指另一個對iOS數(shù)據(jù)持久化封裝的庫梗劫,你可以用Github常用的開源庫享甸,也可以自己寫。 - 其實(shí)對于取出來的數(shù)據(jù)上需要轉(zhuǎn)換的梳侨,由Object轉(zhuǎn)換成T類型就行了蛉威。
- 對于設(shè)值,首先判斷是不是臨時保存的走哺,如果不是蚯嫌,那么需要將其保存到存儲倉庫,最后再將hasValue設(shè)成true就行了。
上面就是這個泛型類最核心的功能的代碼實(shí)現(xiàn)择示。這里面其實(shí)是對最常見的valueForKey和setValueForKey二次封裝束凑,并且再將數(shù)據(jù)再轉(zhuǎn)換成T類型。因?yàn)樵谟脴?gòu)造器實(shí)例化這個類時栅盲,
它就會根據(jù)傳入的默認(rèn)值得知保存的數(shù)據(jù)是什么類型的汪诉。所以便可以正確地轉(zhuǎn)換成原先的類型。
至于這些細(xì)節(jié)實(shí)現(xiàn)代碼谈秫,比如數(shù)據(jù)值變化時的監(jiān)視扒寄,每次設(shè)定值時更新緩存過期的時間,怎么將數(shù)據(jù)保存到存儲倉庫里拟烫,以及清空功能等该编,讀者有興趣可以去看源碼GrandStore。 我在里面是采用歸檔的形式來將保存各種數(shù)據(jù)的硕淑。并且在我做的所有項(xiàng)目里都用了這個庫课竣。不過需要注意的是,因?yàn)镺bjective-C沒有泛型支持置媳,所以目前GrandStore可能不支持Objective-C稠氮。只能用到Swift環(huán)境中。如果覺得不錯半开,請給個Star哦!
使用GrandStore
使用GrandStore十分簡單赃份,可以直接寫在全局變量里面寂拆。所以項(xiàng)目里的任何文件都可以直接訪問。
比如你可以用一個值來記錄APP是不是第一次開啟
let AppFirstLaunch = GrandStore(name: "AppFirstLaunch", defaultValue: true) //默認(rèn)為true,說明App是第一次開啟
//一但app有啟用過抓韩,那么將其設(shè)成false就行了纠永,
AppFirstLaunch.Value = false
//判斷APP是不是第一次啟動
if AppFirstLaunch.Value!{
//第一次啟動,在里面寫一些處理的代碼谒拴。
}
怎么樣尝江,用這個來存儲一些全局的變量可以是說十分的方便,無論是讀取還是寫入英上,都不需要特定的方法炭序,也不需要進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換,直接使用就行苍日。
同樣的惭聂,還可以用它來保存其他的任何類型,比如:
let student = GrandStore(name: "student", defaultValue: Student()) //保存自定義類型
class Student:GrandModel{ //這里注意相恃,我讓Student繼承了GrandModel辜纲,因?yàn)樵谖覍懙腉randStroe中,我是用歸檔來保存對象的
var name:String?
var id:Int = 0
}
let stu = Student()
stu.name = "張三"
stu.id = 1
student.Value = stu //新建一個Student對象再賦值給GrandStore
let stu1 = student.Value! //取出來直接就是Student對象,可以直接操作
stu1.name = "李四"
student.Value = stu1 //修改好的值可以再賦回去實(shí)現(xiàn)永久保存
因?yàn)槲沂怯脷w檔來實(shí)現(xiàn)上面所說的存儲倉庫的耕腾。所以如果我想用GrandStore來保存自定義對象见剩,那么要讓它實(shí)現(xiàn)NSCoding協(xié)議,我在前面寫了一系列的文章打造強(qiáng)大的BaseModel中有實(shí)現(xiàn)自動歸檔的GrandModel扫俺,所以可以在這里直接使用苍苞。
無論是數(shù)組還是字典都毫無壓力
let arrTest = GrandStore(name: "arrTest", defaultValue: [String]()) //保存數(shù)據(jù)
let dictTest = GrandStore(name: "dictTest", defaultValue: [String:String]()) //保存字典
var demoCache = GrandStore(name: "CacheTest", defaultValue: "", timeout: 10) //10秒緩存時間的一個字符串
var demoTemp = GrandStore(name: "demoTemp", defaultValue: "temp", timeout: 0)//只保存在內(nèi)存里的一個字符串
更多的使用示例請參考我的Github,上面有鏈接
總結(jié)
Swift泛型的出現(xiàn)彌補(bǔ)了Objective-C沒有泛型的去缺陷,更是帶來了更多現(xiàn)代編程語言諸多的便捷特性牵舵。使用Swift的泛型柒啤,我們有了一個最好的強(qiáng)類型編程模式。在此基礎(chǔ)上畸颅,泛型方法担巩,閉包,高階函數(shù)
等有了更好的用武之地没炒,極大的提升了開發(fā)效率涛癌。我在此建議會Objective-C的開發(fā)者嘗試去學(xué)習(xí)Swift,掌握它送火,你會發(fā)現(xiàn)另一片天地拳话。