MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift文件操作与IO流

2024-12-087.1k 阅读

Swift 文件操作基础

在Swift编程中,文件操作是一项重要的技能,它允许我们与文件系统进行交互,实现诸如读取、写入、创建和删除文件等操作。Swift 提供了丰富的 API 来处理文件操作,并且这些 API 设计得简洁且易于使用。

路径处理

在进行文件操作之前,首先要处理文件路径。在 Swift 中,URL 类被广泛用于表示文件路径。URL 类提供了许多便捷的方法来操作路径。

// 创建一个表示文件路径的URL
let documentDirectory = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!
let filePath = documentDirectory.appendingPathComponent("example.txt")

在上述代码中,我们首先获取了应用程序的文档目录路径,然后通过 appendingPathComponent 方法添加了文件名,从而构建出完整的文件路径。

文件的基本操作

  1. 检查文件是否存在 在对文件进行操作之前,通常需要先检查文件是否存在。可以使用 FileManager 类来完成此操作。
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath.path) {
    print("文件存在")
} else {
    print("文件不存在")
}

这里通过 fileExists(atPath:) 方法来检查指定路径的文件是否存在,该方法接收一个字符串类型的路径。

  1. 创建文件 创建文件可以使用 FileManagercreateFile(atPath:contents:attributes:) 方法。
let content = "这是一个示例文件内容".data(using:.utf8)!
let success = fileManager.createFile(atPath: filePath.path, contents: content, attributes: nil)
if success {
    print("文件创建成功")
} else {
    print("文件创建失败")
}

在上述代码中,我们将字符串转换为 Data 类型,然后使用 createFile(atPath:contents:attributes:) 方法创建文件。如果文件创建成功,该方法返回 true,否则返回 false

  1. 删除文件 删除文件同样可以使用 FileManager 类。
do {
    try fileManager.removeItem(at: filePath)
    print("文件删除成功")
} catch {
    print("文件删除失败: \(error)")
}

这里使用 removeItem(at:) 方法来删除文件,该方法会抛出错误,因此需要在 do - catch 块中处理。

读取文件

Swift 提供了多种方式来读取文件内容,不同的方式适用于不同的场景。

读取整个文件为字符串

如果文件内容是文本格式,并且文件大小不是特别大,可以直接将整个文件读取为字符串。

do {
    let content = try String(contentsOf: filePath, encoding:.utf8)
    print("文件内容: \(content)")
} catch {
    print("读取文件失败: \(error)")
}

在上述代码中,String(contentsOf:encoding:) 方法尝试从指定的 URL 读取文件内容,并将其转换为字符串。如果读取过程中出现错误,例如文件不存在或者编码错误,会抛出异常并在 catch 块中处理。

读取文件为 Data

当文件内容不是文本格式,或者不确定文件编码时,可以将文件读取为 Data 类型。

do {
    let data = try Data(contentsOf: filePath)
    // 对 data 进行处理,例如解析二进制数据
} catch {
    print("读取文件为Data失败: \(error)")
}

Data(contentsOf:) 方法从指定的 URL 读取文件内容并返回 Data 对象。这种方式适用于读取图像、音频、视频等二进制文件。

逐行读取文本文件

对于较大的文本文件,逐行读取可以减少内存占用。可以使用 BufferedReader 类来实现逐行读取。

import Foundation

class BufferedReader {
    let fileHandle: FileHandle
    var buffer: String = ""

    init?(fileURL: URL) {
        guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else {
            return nil
        }
        self.fileHandle = fileHandle
    }

    deinit {
        fileHandle.closeFile()
    }

    func readLine() -> String? {
        while true {
            if let range = buffer.range(of: "\n") {
                let line = String(buffer[..<range.lowerBound])
                buffer.removeSubrange(..<range.upperBound)
                return line
            }
            let data = fileHandle.readData(ofLength: 4096)
            guard let string = String(data: data, encoding:.utf8) else {
                return nil
            }
            buffer.append(string)
            if data.count < 4096 {
                if buffer.isEmpty {
                    return nil
                }
                let line = buffer
                buffer = ""
                return line
            }
        }
    }
}

let fileURL = Bundle.main.url(forResource: "largeTextFile", withExtension: "txt")!
if let reader = BufferedReader(fileURL: fileURL) {
    while let line = reader.readLine() {
        print(line)
    }
}

在上述代码中,BufferedReader 类封装了文件读取的逻辑,通过 readLine 方法逐行读取文件内容。这种方式适用于处理大型文本文件,避免一次性将整个文件加载到内存中。

写入文件

与读取文件类似,Swift 也提供了多种方式来写入文件。

覆盖写入字符串

如果要将字符串内容写入文件,并且覆盖原有文件内容,可以使用以下方法。

let newContent = "这是新的文件内容"
do {
    try newContent.write(to: filePath, atomically: true, encoding:.utf8)
    print("字符串写入成功")
} catch {
    print("字符串写入失败: \(error)")
}

write(to:atomically:encoding:) 方法将字符串写入指定的文件路径。atomically 参数如果设置为 true,则会先将内容写入临时文件,然后再将临时文件重命名为目标文件,这样可以保证写入操作的原子性,避免文件损坏。

追加写入字符串

如果要在文件末尾追加内容,而不是覆盖原有内容,可以先读取原有内容,然后再加上新内容一起写入。

do {
    var content = try String(contentsOf: filePath, encoding:.utf8)
    content.append("\n这是追加的内容")
    try content.write(to: filePath, atomically: true, encoding:.utf8)
    print("追加写入成功")
} catch {
    print("追加写入失败: \(error)")
}

上述代码先读取文件原有的内容,然后在末尾追加新的内容,最后将合并后的内容写回文件。

写入 Data 到文件

当需要写入二进制数据,例如图像数据、音频数据等,可以使用以下方法。

let imageData = UIImagePNGRepresentation(UIImage(named: "exampleImage")!)!
do {
    try imageData.write(to: filePath)
    print("Data写入成功")
} catch {
    print("Data写入失败: \(error)")
}

write(to:) 方法将 Data 对象写入指定的文件路径。这种方式适用于保存二进制文件。

Swift 中的 IO 流

IO 流(Input/Output Streams)在 Swift 中提供了一种更灵活、更底层的方式来处理输入和输出操作。它允许我们以流的方式逐步处理数据,而不是一次性加载整个文件。

输入流(InputStream)

InputStream 用于从数据源(如文件、网络连接等)读取数据。

let fileURL = Bundle.main.url(forResource: "example", withExtension: "txt")!
let inputStream = InputStream(url: fileURL)!
inputStream.open()

let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
while inputStream.hasBytesAvailable {
    let bytesRead = inputStream.read(&buffer, maxLength: bufferSize)
    if bytesRead > 0 {
        let data = Data(bytes: buffer, count: bytesRead)
        if let string = String(data: data, encoding:.utf8) {
            print(string)
        }
    }
}

inputStream.close()

在上述代码中,我们首先创建了一个 InputStream 对象,并通过 open() 方法打开流。然后在一个循环中,使用 read(_:maxLength:) 方法从流中读取数据到缓冲区,每次读取 bufferSize 大小的数据。读取到的数据可以转换为 Data 类型,进而转换为字符串进行处理。最后,使用 close() 方法关闭流。

输出流(OutputStream)

OutputStream 用于将数据写入到目标(如文件、网络连接等)。

let outputFilePath = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!.appendingPathComponent("output.txt")
let outputStream = OutputStream(url: outputFilePath, append: false)!
outputStream.open()

let contentToWrite = "这是要写入输出流的内容".data(using:.utf8)!
outputStream.write(contentToWrite.bytes, maxLength: contentToWrite.count)

outputStream.close()

这里我们创建了一个 OutputStream 对象,并通过 open() 方法打开流。然后使用 write(_:maxLength:) 方法将数据写入流中,数据可以是 Data 类型的字节数组。最后关闭流。

同时使用输入流和输出流

在一些场景下,例如数据转换或者网络数据传输,可能需要同时使用输入流和输出流。

let inputFileURL = Bundle.main.url(forResource: "input", withExtension: "txt")!
let outputFileURL = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!.appendingPathComponent("output.txt")

let inputStream = InputStream(url: inputFileURL)!
let outputStream = OutputStream(url: outputFileURL, append: false)!

inputStream.open()
outputStream.open()

let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
while inputStream.hasBytesAvailable {
    let bytesRead = inputStream.read(&buffer, maxLength: bufferSize)
    if bytesRead > 0 {
        let data = Data(bytes: buffer, count: bytesRead)
        // 这里可以对数据进行处理,例如加密、转换等
        outputStream.write(data.bytes, maxLength: data.count)
    }
}

inputStream.close()
outputStream.close()

上述代码实现了从一个文件读取数据,经过处理(这里未进行实际处理)后写入到另一个文件的过程。通过同时使用 InputStreamOutputStream,我们可以灵活地处理数据的输入和输出。

处理文件属性

除了对文件内容的操作,Swift 还允许我们获取和修改文件的属性。

获取文件属性

可以使用 FileManagerattributesOfItem(atPath:) 方法来获取文件的属性。

do {
    let attributes = try fileManager.attributesOfItem(atPath: filePath.path)
    if let creationDate = attributes[FileAttributeKey.creationDate] as? Date {
        print("文件创建日期: \(creationDate)")
    }
    if let fileSize = attributes[FileAttributeKey.size] as? NSNumber {
        print("文件大小: \(fileSize) 字节")
    }
} catch {
    print("获取文件属性失败: \(error)")
}

在上述代码中,我们通过 attributesOfItem(atPath:) 方法获取文件的属性字典,然后从字典中获取创建日期和文件大小等属性。

修改文件属性

修改文件属性可以使用 FileManagersetAttributes(_:ofItemAtPath:) 方法。

var fileAttributes = FileManager.default.attributesOfItem(atPath: filePath.path)!
fileAttributes[FileAttributeKey.posixPermissions] = 0o644 as NSNumber
do {
    try fileManager.setAttributes(fileAttributes, ofItemAtPath: filePath.path)
    print("文件属性修改成功")
} catch {
    print("文件属性修改失败: \(error)")
}

这里我们先获取文件的原有属性,然后修改了文件的权限属性,最后使用 setAttributes(_:ofItemAtPath:) 方法将修改后的属性设置回文件。

目录操作

在文件系统中,目录是文件的容器,Swift 提供了丰富的 API 来操作目录。

创建目录

可以使用 FileManagercreateDirectory(at:withIntermediateDirectories:attributes:) 方法来创建目录。

let newDirectoryPath = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!.appendingPathComponent("newDirectory")
do {
    try fileManager.createDirectory(at: newDirectoryPath, withIntermediateDirectories: true, attributes: nil)
    print("目录创建成功")
} catch {
    print("目录创建失败: \(error)")
}

createDirectory(at:withIntermediateDirectories:attributes:) 方法中,withIntermediateDirectories 参数如果设置为 true,则会自动创建不存在的父目录。

列出目录内容

使用 FileManagercontentsOfDirectory(at:includingPropertiesForKeys:options:) 方法可以列出目录中的内容。

do {
    let directoryContents = try fileManager.contentsOfDirectory(at: newDirectoryPath, includingPropertiesForKeys: nil, options: [])
    for item in directoryContents {
        print(item.lastPathComponent)
    }
} catch {
    print("列出目录内容失败: \(error)")
}

上述代码列出了指定目录中的所有文件和子目录的名称。

删除目录

删除目录同样使用 FileManagerremoveItem(at:) 方法。

do {
    try fileManager.removeItem(at: newDirectoryPath)
    print("目录删除成功")
} catch {
    print("目录删除失败: \(error)")
}

需要注意的是,如果目录不为空,removeItem(at:) 方法会抛出错误,除非使用 FileManagerremoveItem(at:withReuseOfContentsOf:) 方法,该方法会递归删除目录及其所有内容。

错误处理与最佳实践

在文件操作和 IO 流处理过程中,可能会遇到各种错误,如文件不存在、权限不足、编码错误等。正确处理这些错误是保证程序稳定性和健壮性的关键。

错误处理

Swift 使用 do - catch 块来处理错误。在前面的代码示例中,我们已经多次使用了这种方式。例如,在读取文件时:

do {
    let content = try String(contentsOf: filePath, encoding:.utf8)
    // 处理文件内容
} catch {
    print("读取文件失败: \(error)")
}

catch 块中,可以根据错误类型进行不同的处理,例如记录日志、提示用户等。

最佳实践

  1. 检查文件和目录是否存在:在进行读取、写入、删除等操作之前,始终先检查文件或目录是否存在,以避免不必要的错误。
  2. 使用原子操作:在写入文件时,尽量使用原子操作(如 write(to:atomically:encoding:) 方法中的 atomically 参数设置为 true),以防止文件损坏。
  3. 资源管理:对于打开的文件句柄、流等资源,确保在使用完毕后及时关闭,以避免资源泄漏。
  4. 权限处理:在进行文件操作时,要注意文件和目录的权限设置,确保程序有足够的权限进行相应的操作。如果权限不足,要给出合适的提示或处理方式。

通过遵循这些最佳实践,可以提高文件操作和 IO 流处理的可靠性和稳定性,使程序更加健壮。

在Swift编程中,文件操作和 IO 流是非常重要的部分,掌握这些知识可以让我们更好地与文件系统进行交互,实现各种功能,无论是简单的文本文件处理还是复杂的二进制数据传输。希望通过本文的介绍,你对 Swift 中的文件操作与 IO 流有了更深入的理解和掌握。