如何写出最简洁优雅的android 网络请求封装封装 Moya + RxSwift

用 RxSwift + Moya 写出优雅的网络请求代码 - 简书
用 RxSwift + Moya 写出优雅的网络请求代码
Rx 是微软出品的一个 Funtional Reactive Programming 框架,RxSwift 是它的一个 Swift 版本的实现。RxSwift 的主要目的是能简单的处理多个异步操作的组合,和事件/数据流。利用 RxSwift,我们可以把本来要分散写到各处的代码,通过方法链式调用来组合起来,非常的好看优雅。
举个例子,有如下操作:点击按钮 -& 发送网络请求 -& 对返回的数据进行某种格式处理 -& 显示在一个 UILabel 上
代码如下:
sendRequestButton
.flatMap(viewModel.loadData)
.throttle(0.3, scheduler: MainScheduler.instance)
.map { "\($0.debugDescription)" }
.bindTo(self.resultLabel.rx_text)
.addDisposableTo(disposeBag)
是不是看上去很优雅呢?
另外中也有一个类似的例子:
对应的代码是:
.rx_tap // 点击登录
.flatMap(provider.login) // 登录请求
.map(saveToken) // 保存 token
.flatMap(provider.requestInfo) // 获取用户信息
.subscribe(handleResult) // 处理结果
用一连串的链式调用就把一系列事件处理了,是不是很不错。
Moya 是 Artsy 团队的 Ash Furrow 主导开发的一个网络抽象层库。它在 Alamofire 基础上提供了一系列简单的抽象接口,让客户端代码不用去直接调用 Alamofire,也不用去关心 NSURLSession。同时提供了很多实用的功能。它的 Target -& Endpoint -& Request 模式也使得每个请求都可以自由定制。
下面进入正题:
创建一个请求
Moya 的 TargetType 协议规定的创建网络请求的方法,用枚举来创建,很有 Swift 的风格。
enum DataAPI {
extension DataAPI: TargetType {
var baseURL: NSURL { return NSURL(string: "http://localhost:3000")! }
var path: String {
return "/data"
var method: Moya.Method {
return .GET
var parameters: [String : AnyObject]? {
return nil
var sampleData: NSData {
return stubbedResponseFromJSONFile("stub_data")
var multipartBody: [Moya.MultipartFormData]? {
return nil
创建数据模型
数据模型的创建用了 SwiftyJSON 和 Moya_SwiftyJSONMapper,方便将 JSON 直接映射成 Model 对象。
struct DataModel: ALSwiftyJSONAble {
var title: String?
var content: String?
init?(jsonData: JSON) {
self.title = jsonData["title"].string
self.content = jsonData["content"].string
我们可使用 Moya 自带一个 RxSwift 的扩展来发送请求。
class ViewModel {
private let provider = RxMoyaProvider&DataAPI&() // 创建为 RxSwift 扩展的 MoyaProvider
func loadData() -& Observable&DataModel& {
return provider
.request(.DataRequest) // 通过某个 Target 来指定发送哪个请求
.debug() // 打印请求发送中的调试信息
.mapObject(DataModel) // 请求的结果映射为 DataModel 对象
然后在 ViewController 中就可以写上面说到过的那一段了
sendRequestButton
.rx_tap // 观察按钮点击信号
.flatMap(viewModel.loadData) // 调用 loadData
.map { "\($0.title) \($0.content)" } // 格式化显示内容
.bindTo(self.resultLabel.rx_text) // 绑定到 UILabel 上
.addDisposableTo(disposeBag) // 添加到 disposeBag,当 disposeBag 释放时,这个绑定关系也会被释放
这样就实现了 点击按钮 -& 发送网络请求 -& 显示结果上面这一段没有考虑错误处理,这个后面会说。
URL 缓存则是采用 Alamofire 的缓存处理方式——用系统缓存(NSURLCache)。NSURLCache 默认采用的缓存策略是 NSURLRequestUseProtocolCachePolicy。缓存的具体方式可以由服务端在返回的响应头部添加 Cache-Control 字段来控制。
有一种缓存是系统的缓存做不到的,就是离线缓存。离线缓存的流程是:发请求前先看看本地有没有离线缓存有 -& 使用离线缓存数据渲染界面 -& 发出网络请求 -& 用请求到的数据更新界面无 -& 发出网络请求 -& 用请求到的数据更新界面
由于 Moya 没有提供离线缓存这个功能,只能自己写了。为 RxMoyaProvider 扩展离线缓存功能:
extension RxMoyaProvider {
func tryUseOfflineCacheThenRequest(token: Target) -& Observable&Moya.Response& {
return Observable.create { [weak self] observer -& Disposable in
let key = token.cacheKey // 缓存 Key,可以根据自己的需求来写,这里采用的是 BaseURL + Path + Parameter转化为JSON字符串
// 先读取缓存内容,有则发出一个信号(onNext),没有则跳过
if let response = HSURLCache.sharedInstance.cachedResponseForKey(key) {
observer.onNext(response)
// 发出真正的网络请求
let cancelableToken = self?.request(token) { result in
switch result {
case let .Success(response):
observer.onNext(response)
observer.onCompleted()
HSURLCache.sharedInstance.cacheResponse(response, forKey: key)
case let .Failure(error):
observer.onError(error)
return AnonymousDisposable {
cancelableToken?.cancel()
以上代码创建了一个信号序列,当有离线缓存时,会发出一个信号,当网络请求结果返回时,会发出一个信号,当网络请求失败时,也会发出一个错误信号。
上面的 HSURLCache 是我自己写的一个缓存类,通过 SQLite 把 Moya 的 Response 对象保存到数据库中。
由于 Moya 的 Response 对象是被 `final` 修饰的,无法通过继承方式为其添加 NSCoder 实现。所以就将 Response 的三个属性分别保存。
读缓存数据时也是读出三个属性的数据,再用他们创建成 Response 对象。
func loadData() -& Observable&DataModel& {
return provider
.tryUseOfflineCacheThenRequest(.DataRequest)
.distinctUntilChanged()
.mapObject(DataModel)
使用离线缓存的网络请求方式可以写成这样,调用了上面所说的 tryUseOfflineCacheThenRequest 方法。并且这里用了 RxSwift 的 distinctUntilChanged 方法,当两个信号完全一样时,会过滤掉后面的信号。这样避免页面在数据相同的情况下渲染两次。
可以通过判断 event 对象来处理错误,代码如下:
sendRequestButton
.flatMap(viewModel.loadData)
.throttle(0.3, scheduler: MainScheduler.instance)
.map { "\($0.title) \($0.content)" }
.subscribe { event in
switch event {
case .Next(let data):
print(data)
case .Error(let error):
print(error)
case .Completed:
.addDisposableTo(disposeBag)
本地假数据
这时 Moya 的一个功能,可以在本地放置一个 json 文件,网络请求可以设置成读取本地文件内容来返回数据。可以在接口故障或为开发完时,客户端可以先用假数据来开发,先走通流程。
只要在创建 RxMoyaProvider 时指定一个参数 stubClosure。
使用本地假数据:
RxMoyaProvider&DataAPI&(stubClosure: MoyaProvider.ImmediatelyStub)
使用网络接口真实数据:
RxMoyaProvider&DataAPI&(stubClosure: MoyaProvider.NeverStub)
Moya 也提供了一个模拟网络延迟的方法。使用本地假数据并有 3 秒的延迟:
RxMoyaProvider&DataAPI&(stubClosure: MoyaProvider.DelayedStub(3))
Header 处理
例如如果想要在 Header 中添加一些字段,例如 access-token,可以通过 Moya 的 Endpoint Closure 方式实现,代码如下:
let commonEndpointClosure = { (target: Target) -& Endpoint&Target& in
var URL = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
let endpoint = Endpoint&Target&(URL: URL,
sampleResponseClosure: {.NetworkResponse(200, target.sampleData)},
method: target.method,
parameters: target.parameters)
// 添加 AccessToken
if let accessToken = currentUser.accessToken {
return endpoint.endpointByAddingHTTPHeaderFields(["access-token": accessToken])
return endpoint
另外 Moya 的插件机制也很好用,提供了两个接口,willSendRequest 和 didReceiveResponse,可以在请求发出前和请求收到后做一些额外的处理,并且不和主功能耦合。
Moya 本身提供了打印网路请求日志的插件和 NetworkActivityIndicator 的插件。
例如检测 access-token 的合法性:
internal final class AccessTokenPlugin: PluginType {
func willSendRequest(request: RequestType, target: TargetType) {
func didReceiveResponse(result: Result&RxMoya.Response, RxMoya.Error&, target: TargetType) {
switch result {
case .Success(let response):
let jsonObject = try response.mapJSON()
let json = JSON(jsonObject)
if json["status"].intValue == InvalidStatus {
NSNotificationCenter.defaultCenter().postNotificationName("InvalidTokenNotification", object: nil)
case .Failure(_):
然后在创建 RxMoyaProvider 时注册插件:
private let provider = RxMoyaProvider&DataAPI&(stubClosure: MoyaProvider.NeverStub, plugins: [AccessTokenPlugin()])
对于用 Swift 编写的项目来说,可以有比 Objective-C 更优雅的方式来编写网络层代码。RxSwift + Moya 是个不错的选择,不仅能使代码更优雅美观,方便维护,还有具有一些很实用的小功能。
iOS 程序员,写写代码。
博客:https://liuduo.me主题信息(必填)
主题描述(最多限制在50个字符)
申请人信息(必填)
申请信息已提交审核,请注意查收邮件,我们会尽快给您反馈。
如有疑问,请联系
CSDN &《程序员》编辑/记者,投稿&纠错等事宜请致邮
傻丫头和高科技产物小心翼翼的初恋
如今的编程是一场程序员和上帝的竞赛,程序员要开发出更大更好、傻瓜都会用到软件。而上帝在努力创造出更大更傻的傻瓜。目前为止,上帝是赢的。个人网站:。个人QQ群:、
个人大数据技术博客:
人生得意须尽欢,莫使金樽空对月。网络请求(6)
1.关于moya
如在OC中使用AFNetworking一般,Swift我们用Alamofire来做网络库.而Moya在Alamofire的基础上又封装了一层:
官方说moya有以下特性(我也就信了):
编译时检查正确的API端点访问.
使你定义不同端点枚举值对应相应的用途更加明晰.
提高测试地位从而使单元测试更加容易.
1.创建枚举API
enum APIManager {
case getNewsLatest
case getStartImage
case getVersion(String)
case getThemes
case getNewsDetail(Int)
2.实现TargetType协议
extension APIManager: TargetType {
The target's base `URL`.
var baseURL: URL {
return URL.init(string: "http://news-/api/")!
The path to be appended to `baseURL` to form the full `URL`.
var path: String {
switch self {
case .getNewsLatest:
return "4/news/latest"
case .getStartImage:
return "4/start-image/"
case .getVersion(let version):
return "4/version/ios/" + version
case .getThemes:
return "4/themes"
case .getNewsDetail(let id):
return "4/news/\(id)"
The HTTP method used in the request.
var method: Moya.Method {
return .get
The parameters to be incoded in the request.
var parameters: [String: Any]? {
return nil
The method used for parameter encoding.
var parameterEncoding: ParameterEncoding {
return URLEncoding.default
Provides stub data for use in testing.
var sampleData: Data {
return "".data(using: String.Encoding.utf8)!
The type of HTTP task to be performed.
var task: Task {
return .request
Whether or not to perform Alamofire validation. Defaults to `false`.
var validate: Bool {
return false
在这里,可以设置请求的参数,例如url……method……para等.
Moya的使用非常简单,通过TargetType协议定义好每个target之后,就可以直接使用Moya开始发送网络请求了。就像这样:
let provider = MoyaProvider&APIManager&()
provider.request(.getNewsLatest) { result in
// do something with result
3.配合RxSwift
Moya本身已经是一个使用起来非常方便,能够写出非常简洁优雅的代码的网络封装库,但是让Moya变得更加强大的原因之一还因为它对于Functional Reactive Programming的扩展,具体说就是对于RxSwift和ReactiveCocoa的扩展,通过与这两个库的结合,能让Moya变得更加强大。我选择RxSwift的原因有两个,一个是RxSwift的库相对来说比较轻量级,语法更新相对来说比较少,我之前用过ReactiveCocoa,一些大版本的更新需求重写很多代码,第二个更重要的原因是因为RxSwift背后有整个ReactiveX的支持,里面包括Java,JS,.Net, Swift,Scala,它们内部都用了ReactiveX的逻辑思想,这意味着你一旦学会了其中的一个,以后可以很快的上手ReactiveX中的其他语言。
Moya提供了非常方面的RxSwift扩展:
let provider = RxMoyaProvider&APIManager&()
provider.request(.getNewsLatest)
.filterSuccessfulStatusCodes()
.mapJSON()
.subscribe(onNext: { (json) in
//do something with posts
print(json)
.addDisposableTo(disposeBag)
RxMoyaProvider是MoyaProvider的子类,是对RxSwift的扩展
filterSuccessfulStatusCodes()是Moya为RxSwift提供的扩展方法,顾名思义,可以得到成功地网络请求,忽略其他的
mapJSON()也是Moya RxSwift的扩展方法,可以把返回的数据解析成 JSON 格式
subscribe 是一个RxSwift的方法,对经过一层一层处理的 Observable 订阅一个 onNext 的 observer,一旦得到 JSON 格式的数据,就会经行相应的处理
addDisposableTo(disposeBag) 是 RxSwift 的一个自动内存处理机制,跟ARC有点类似,会自动清理不需要的对象。
4.配合HandyJSON
在实际应用过程中网络请求往往紧密连接着数据层(Model),具体地说,在我们的这个例子中,一般我们需要建立一个类用来统一管理数据,然后把得到的 JSON 数据映射到数据层(Model)。
struct MenuModel: HandyJSON {
var others: [ThemeModel]?
struct ThemeModel: HandyJSON {
var color: String?
var thumbnail: String?
var id: Int?
var description: String?
var name: String?
然后创建ViewModel类,创建具体请求方法:
class MenuViewModel {
private let provider = RxMoyaProvider&APIManager&()
var dispose = DisposeBag()
func getThemes(completed: @escaping (_ menuModel: MenuModel) -& ()){
.request(.getThemes)
.mapModel(MenuModel.self)
.subscribe(onNext: { (model) in
completed(model)
}, onError: { (error) in
}, onCompleted: nil, onDisposed: nil).addDisposableTo(dispose)
这里解释一下:
我这里是将请求的数据通过闭包传了出去,当然也可以不那么做.个人喜好问题..
这里是为 RxSwift 中的 ObservableType和 Response写一个简单的扩展方法 mapModel,利用我们写好的Model 类,一步就把JSON数据映射成 model。
extension ObservableType where E == Response {
public func mapModel&T: HandyJSON&(_ type: T.Type) -& Observable&T& {
return flatMap { response -& Observable&T& in
return Observable.just(response.mapModel(T.self))
extension Response {
func mapModel&T: HandyJSON&(_ type: T.Type) -& T {
let jsonString = String.init(data: data, encoding: .utf8)
return JSONDeserializer&T&.deserializeFrom(json: jsonString)!
5.配合ObjectMapper
毕竟将json数据转换成model的库那么多 ….,所以……,用哪个很随意…..这里再介绍一下ObjectMapper
1.创建model类
class DetailModel: Mappable {
var body = String()
var image_source: String?
var title = String()
var image: String?
var share_url = String()
var js = String()
var recommenders = [[String: String]]()
var ga_prefix = String()
var section: DetailSectionModel?
var type = Int()
var id = Int()
var css = [String]()
func mapping(map: Map) {
body &- map["body"]
image_source &- map["image_source"]
title &- map["title"]
image &- map["image"]
share_url &- map["share_url"]
js &- map["js"]
recommenders &- map["recommenders"]
ga_prefix &- map["ga_prefix"]
section &- map["section"]
type &- map["type"]
id &- map["id"]
css &- map["css"]
required init?(map: Map) {
使用 ObjectMapper ,需要让自己的 Model 类使用 Mappable 协议,这个协议包括两个方法:
required init?(map: Map) {}
func mapping(map: Map) {}
在 mapping 方法中,用 &- 操作符来处理和映射你的 JSON数据。
数据类建立好之后,我们还需要为 RxSwift 中的 Observable 写一个简单的扩展方法 mapObject,利用我们写好的model 类,一步就把JSON 数据映射成一个个 model。
extension Observable {
func mapObject&T: Mappable&(type: T.Type) -& Observable&T& {
return self.map { response in
//if response is a dictionary, then use ObjectMapper to map the dictionary
//if not throw an error
guard let dict = response as? [String: Any] else {
throw RxSwiftMoyaError.ParseJSONError
return Mapper&T&().map(JSON: dict)!
func mapArray&T: Mappable&(type: T.Type) -& Observable&[T]& {
return self.map { response in
//if response is an array of dictionaries, then use ObjectMapper to map the dictionary
//if not, throw an error
guard let array = response as? [Any] else {
throw RxSwiftMoyaError.ParseJSONError
guard let dicts = array as? [[String: Any]] else {
throw RxSwiftMoyaError.ParseJSONError
return Mapper&T&().mapArray(JSONArray: dicts)!
enum RxSwiftMoyaError: String {
case ParseJSONError
case OtherError
extension RxSwiftMoyaError: Swift.Error { }
mapObject 方法处理单个对象,mapArray 方法处理对象数组。
如果传进来的数据 response 是一个 dictionary,那么就利用 ObjectMapper 的 map方法映射这些数据,这个方法会调用你之前在 mapping方法里面定义的逻辑。
如果 response 不是一个 dictionary, 那么就抛出一个错误。
在底部自定义了简单的Error,继承了Swift 的 Error类,在实际应用过程中可以根据需要提供自己想要的 Error。
然后运行请求方法:
class DetailViewModel {
private let provider = RxMoyaProvider&APIManager&()
func getNewsDetail(id: Int) -& Observable&DetailModel& {
return provider
.request(.getNewsDetail(id))
.filterSuccessfulStatusCodes()
.mapJSON()
.mapObject(type: DetailModel.self)
有没有感觉很爽呢!————
有不对之处,,,,还望各路大神不吝指正!
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:17092次
排名:千里之外
原创:56篇
(1)(8)(3)(8)(16)(11)(14)(4)swift(6)
//moya-rxswift-argo-lets-go/
最近在新项目中尝试使用 Moya+RxSwift+Argo 进行网络请求和解析,感觉还阔以,再来给大家安利一波。
Moya&是一个基于&Alamofire&的更高层网络请求封装,深入学习请参见官方文档:。
使用&Moya&之后网络请求一般长了这样:
provider.request(.UserProfile(&ashfurrow&)) { (data, statusCode, response, error) in
if let data = data {
Moya&提供了很多不错的特性,其中我感觉最棒的是&stub&,配合&sampleData&分分钟就完成了单元测试:
private let provider = MoyaProvider&ItemAPI&(stubClosure: MoyaProvider.ImmediatelyStub)
注意这里的&MoyaProvider.ImmediatelyStub&,我原以为它是个枚举类型,看了&MoyaProvider&定义发现这里应该传个&closure&,看了&ImmediatelyStub&的定义发现原来它是个类方法:
public typealias StubClosure = Target -& Moya.StubBehavioroverride public init(stubClosure: StubClosure = MoyaProvider.NeverStub, ...) {}public final class func ImmediatelyStub(_: Target) -& Moya.StubBehavior {
return .Immediate}
如果想打印每次请求的参数,在组装&endpoint&的时候打印即可:
private func endpointMapping&Target: MoyaTarget&(target: Target) -& Endpoint&Target& {
if let parameters = target.parameters {
log.verbose(&\(parameters)&)
return MoyaProvider.DefaultEndpointMapping(target)}private let provider = RxMoyaProvider&ItemAPI&(endpointClosure: endpointMapping)
RxSwift&前面强行过两波,在此不再赘述啦,Moya&本身提供了&RxSwift&扩展,可以无缝衔接&RxSwift和&ReactiveCocoa&,于是打开方式变成了这样:
private let provider = RxMoyaProvider&ItemAPI&()private var disposeBag = DisposeBag()extension ItemAPI {
static func getNewItems(completion: [Item] -& Void) {
disposeBag = DisposeBag()
.request(.GetItems())
.subscribe(
onNext: { items in
completion(items)
.addDisposableTo(disposeBag)
Moya&的核心开发者、同时也是&&的成员:, 在 AltConf 做过一次 《》 的分享,推荐大家看一下,很可爱的!
Argo&是&thoughtbot&开源的函数式&JSON&解析转换库。说到&thoughtbot&就不得不提他司关于&JSON&解析质量很高的一系列文章:
Argo&基本上就是沿着这些文章的思路写出来的,相关的库还有&&和&。
使用&Argo&做&JSON&解析很有意思,大致长这样:
struct Item {
let id: String
let url: String}extension Item: Decodable {
static func decode(j: JSON) -& Decoded&Item& {
return curry(Item.init)
&^& j &| &id&
&*& j &| &url&
至于这其中各种符号的缘由,在几篇博客中都有讲解,还是挺有意思滴。
说完这三者,如何把它们串起来呢?&中的&&给了我们答案。稍微整理后如下:
enum ORMError : ErrorType {
case ORMNoRepresentor
case ORMNotSuccessfulHTTP
case ORMNoData
case ORMCouldNotMakeObjectError}extension Observable {
private func resultFromJSON&T: Decodable&(object:[String: AnyObject], classType: T.Type) -& T? {
let decoded = classType.decode(JSON.parse(object))
switch decoded {
case .Success(let result):
return result as? T
case .Failure(let error):
log.error(&\(error)&)
return nil
func mapSuccessfulHTTPToObject&T: Decodable&(type: T.Type) -& Observable&T& {
return map { representor in
guard let response = representor as? MoyaResponse else {
throw ORMError.ORMNoRepresentor
guard ((200...209) ~= response.statusCode) else {
if let json = try? NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] {
log.error(&Got error message: \(json)&)
throw ORMError.ORMNotSuccessfulHTTP
guard let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] else {
throw ORMError.ORMCouldNotMakeObjectError
return self.resultFromJSON(json, classType:type)!
throw ORMError.ORMCouldNotMakeObjectError
func mapSuccessfulHTTPToObjectArray&T: Decodable&(type: T.Type) -& Observable&[T]& {
return map { response in
guard let response = response as? MoyaResponse else {
throw ORMError.ORMNoRepresentor
guard ((200...209) ~= response.statusCode) else {
if let json = try? NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [String: AnyObject] {
log.error(&Got error message: \(json)&)
throw ORMError.ORMNotSuccessfulHTTP
guard let json = try NSJSONSerialization.JSONObjectWithData(response.data, options: .AllowFragments) as? [[String : AnyObject]] else {
throw ORMError.ORMCouldNotMakeObjectError
var objects = [T]()
for dict in json {
if let obj = self.resultFromJSON(dict, classType:type) {
objects.append(obj)
return objects
throw ORMError.ORMCouldNotMakeObjectError
这样在调用的时候就很舒服了,以前面的&Item&为例:
private let provider = RxMoyaProvider&ItemAPI&()private var disposeBag = DisposeBag()extension ItemAPI {
static func getNewItems(records:[Record] = [], needCount: Int, completion: [Item] -& Void) {
disposeBag = DisposeBag()
.request(.AddRecords(records, needCount))
.mapSuccessfulHTTPToObjectArray(Item)
.subscribe(
onNext: { items in
completion(items)
.addDisposableTo(disposeBag)
一个&mapSuccessfulHTTPToObjectArray&方法,直接将&JSON&字符串转换成了&Item&对象,并且传入了后面的数据流中,所以在&onNext&订阅的时候传入的就是&[Item]&数据,并且这个转换过程还是可以复用的,且适用于所有网络请求中&JSON&和&Model&的转换。爽就一个字,我只说一次。
匆匆读了一点&&和&&的项目源码,没有深入不过已经受益匪浅。通过
bundle 管理 id 和 key 直接解决了我当初纠结已久的『完整项目开源如何优雅地保留 git 记录且保护项目隐私』的问题,还有&Moya/RxSwift&和&Moya/ReactiveCocoa&这种子模块化处理也在共有模块管理这个问题上给了我一些启发。
真是很喜欢 Artsy 这样的团队,大家都一起做着自己喜欢的事情,还能站着把钱赚了。
所幸的是我也可以这样做自己喜欢的事情了,不过不赚钱。具体状况后面单独开一篇闲扯扯。
参考资料:
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:92033次
积分:1428
积分:1428
排名:千里之外
原创:19篇
转载:220篇
(5)(4)(14)(5)(8)(4)(6)(9)(15)(16)(7)(10)(25)(8)(2)(2)(1)(2)(5)(3)(9)(29)(13)(37)

我要回帖

更多关于 如何写出优雅的程序 的文章

 

随机推荐