Swiftで「Sequence」プロトコルを使って独自のシーケンスを実装することは、コードの柔軟性と拡張性を高める強力な方法です。シーケンスとは、コレクションのように順序付きで要素を一つずつ提供するものを指し、「for-in」ループや多くの高階関数で利用できます。標準ライブラリに含まれるArrayやSetなどの型もこのプロトコルに準拠していますが、独自のデータ構造に合わせたシーケンスを作ることで、特定の用途に適したデータ処理が可能になります。本記事では、「Sequence」プロトコルを実際にどのように実装し、独自のシーケンスを作成するのか、その具体的な方法を解説していきます。
Sequenceプロトコルとは
Sequenceプロトコルは、Swiftにおける順序付きのコレクションデータを表す基本的なプロトコルです。このプロトコルに準拠した型は、要素を順番に提供でき、「for-in」ループやmap
、filter
などの高階関数で簡単に操作できます。シーケンスは一度に一つの要素を順に取得し、シンプルなデータ操作を実現します。具体的には、ArrayやDictionaryなどのコレクション型がすでにSequenceプロトコルに準拠しており、これにより汎用的なデータ処理が可能となります。Sequenceプロトコルの核心は、イテレーターを返すmakeIterator
メソッドです。これにより、シーケンスの各要素にアクセスするための反復処理が提供されます。
Sequenceプロトコルを使うメリット
Sequenceプロトコルを使うことで、コードの再利用性と柔軟性が大幅に向上します。以下は主なメリットです。
再利用性の向上
独自のデータ構造に対してSequenceプロトコルを実装すると、そのデータ構造が標準のSwift関数やループ構文とシームレスに統合されます。例えば、for-in
ループやmap
、filter
などの標準的な操作が使えるようになり、複雑なデータ操作もシンプルな構文で記述できます。
遅延評価によるパフォーマンス改善
Sequenceプロトコルを使うと、必要なときに要素を一つずつ処理できるため、大量のデータを扱う際のパフォーマンスが向上します。データ全体をメモリに読み込むことなく、逐次処理する遅延評価が可能になります。
カスタムデータ構造の柔軟な処理
独自のデータ型やアルゴリズムを、標準のコレクションと同じように操作できるため、データ構造に合わせた最適な処理を実装しやすくなります。
Sequenceプロトコルの要件
Sequenceプロトコルを実装するためには、特定の要件を満たす必要があります。最も重要なのは、シーケンス内の要素に順次アクセスするためのmakeIterator
メソッドの実装です。このメソッドが呼び出されると、要素を一つずつ返す「イテレーター(Iterator)」を生成します。
makeIteratorメソッド
makeIterator
メソッドは、シーケンスの要素を順に処理するためのIterator
を返すメソッドです。このメソッドは、シーケンスが提供する要素を1つずつ取り出し、操作するためのインターフェースを提供します。イテレーター自体は、next()
メソッドを持ち、要素を1つずつ返し、要素がなくなるとnil
を返す仕組みです。
func makeIterator() -> SomeIteratorType {
// イテレーターを返す実装
}
Iteratorプロトコルの実装
Iterator
プロトコルは、next()
メソッドを持つオブジェクトを表します。このメソッドは、シーケンスの次の要素を返し、要素がなくなった場合はnil
を返す必要があります。Iterator
の実装が正しく行われることで、シーケンスの順次処理が可能になります。
struct SomeIteratorType: IteratorProtocol {
func next() -> Element? {
// 次の要素を返す
}
}
これにより、カスタムシーケンスを自由に設計し、標準的なコレクションのように扱えるようになります。
実際のシーケンスを作成する
ここでは、独自のシーケンスを作成する具体的な手順を見ていきます。基本的な流れとしては、まずシーケンスとなる型を定義し、その型がSequence
プロトコルに準拠するようにmakeIterator
メソッドを実装します。次に、シーケンス内で使用するカスタムイテレーターを定義していきます。
ステップ1: シーケンス型の定義
最初に、独自のデータ型を定義し、それにSequence
プロトコルを準拠させます。以下の例では、単純なカウンターシーケンスを作成します。このシーケンスは、特定の範囲の数値を順番に返します。
struct Counter: Sequence {
let start: Int
let end: Int
func makeIterator() -> CounterIterator {
return CounterIterator(start: start, end: end)
}
}
このCounter
型は、指定した範囲(start
からend
まで)の数値を順に返すシーケンスです。
ステップ2: カスタムイテレーターの定義
次に、シーケンスを構成するために必要なイテレーターを定義します。イテレーターは、IteratorProtocol
に準拠し、next()
メソッドを実装する必要があります。
struct CounterIterator: IteratorProtocol {
var current: Int
let end: Int
init(start: Int, end: Int) {
self.current = start
self.end = end
}
mutating func next() -> Int? {
guard current <= end else {
return nil
}
let nextValue = current
current += 1
return nextValue
}
}
このCounterIterator
は、start
からend
までの数値を順に返し、範囲外に達するとnil
を返して終了します。
ステップ3: シーケンスの使用
作成したシーケンスを使用するには、for-in
ループなどで簡単に繰り返し処理を行うことができます。
let counter = Counter(start: 1, end: 5)
for number in counter {
print(number)
}
このコードは、1から5までの数値を順に出力します。このようにして、独自のデータ構造やアルゴリズムに合わせたシーケンスを作成し、Swiftの標準機能と組み合わせて効率的なデータ処理を実現できます。
カスタムイテレーターの実装
カスタムシーケンスの重要な要素は、シーケンスの要素を一つずつ提供するイテレーターです。前のセクションで紹介したように、IteratorProtocol
を実装することでカスタムイテレーターを作成できます。このセクションでは、さらにイテレーターの設計と実装の詳細について説明します。
IteratorProtocolの要件
IteratorProtocol
は、必須メソッドnext()
を提供するプロトコルです。このnext()
メソッドは、シーケンスの次の要素を順番に返し、要素がなくなったときにnil
を返します。この仕組みによって、for-in
ループやその他のシーケンス操作が自然に動作します。
struct CounterIterator: IteratorProtocol {
var current: Int
let end: Int
mutating func next() -> Int? {
guard current <= end else {
return nil
}
let nextValue = current
current += 1
return nextValue
}
}
上記のCounterIterator
の実装では、next()
メソッドが呼ばれるたびに、現在のカウントを返して次の値へ進み、範囲を超えたらnil
を返す設計になっています。
イテレーターの状態管理
イテレーターは状態を持ち、次に返すべき要素を記憶します。例えば、CounterIterator
ではcurrent
がイテレーターの状態を管理する変数です。イテレーターの状態管理が適切に行われていないと、予期しない挙動が発生するため、注意が必要です。シーケンスの長さや終了条件を考慮し、無限ループや無限シーケンスに注意して実装することが重要です。
struct FibonacciIterator: IteratorProtocol {
var previous = 0
var current = 1
mutating func next() -> Int? {
let nextValue = previous + current
previous = current
current = nextValue
return previous
}
}
このFibonacciIterator
は、フィボナッチ数列を生成するイテレーターです。次の値を計算し、以前の値を更新することで、フィボナッチ数列を無限に生成することができます。
カスタムイテレーターの使用例
実際にカスタムイテレーターを使用してシーケンスを操作するには、for-in
ループやその他のシーケンス処理が自然に使えます。
let fibonacciSequence = FibonacciIterator()
for _ in 1...10 {
print(fibonacciSequence.next()!)
}
このコードは、最初の10個のフィボナッチ数を順に出力します。このように、イテレーターを使うことで、複雑なデータ生成や操作が簡単に実現できます。
カスタムイテレーターの実装は、特定の用途に応じたデータ処理や操作をカスタマイズするために非常に有用です。
シーケンスの応用例
カスタムシーケンスを実装することで、さまざまな応用が可能になります。シーケンスの柔軟性を活かして、データの遅延評価や効率的なデータ処理、特定のパターンを生成するなど、多様なニーズに対応できます。ここでは、いくつかの具体的な応用例を見ていきます。
応用例1: ランダム数列生成
カスタムシーケンスを使って、無限にランダムな数値を生成するシーケンスを実装することができます。これにより、必要なだけランダムなデータを取り出すことが可能です。
struct RandomNumberSequence: Sequence {
let range: ClosedRange<Int>
func makeIterator() -> RandomNumberIterator {
return RandomNumberIterator(range: range)
}
}
struct RandomNumberIterator: IteratorProtocol {
let range: ClosedRange<Int>
mutating func next() -> Int? {
return Int.random(in: range)
}
}
このシーケンスを使えば、必要な数だけランダムな数値を簡単に生成できます。
let randomNumbers = RandomNumberSequence(range: 1...100)
for number in randomNumbers.prefix(10) {
print(number)
}
このコードは、1から100の範囲内で10個のランダムな数を出力します。
応用例2: フィルタリングされたシーケンス
シーケンスをさらに強力にする方法の一つは、要素をフィルタリングして、特定の条件に一致する要素のみを返すカスタムシーケンスを作成することです。例えば、偶数のみを返すシーケンスを作成できます。
struct EvenNumberSequence: Sequence {
let start: Int
let end: Int
func makeIterator() -> EvenNumberIterator {
return EvenNumberIterator(current: start, end: end)
}
}
struct EvenNumberIterator: IteratorProtocol {
var current: Int
let end: Int
mutating func next() -> Int? {
while current <= end {
let value = current
current += 1
if value % 2 == 0 {
return value
}
}
return nil
}
}
このシーケンスは、指定した範囲内の偶数のみを返します。
let evenNumbers = EvenNumberSequence(start: 1, end: 10)
for number in evenNumbers {
print(number)
}
結果として、1から10までの範囲内の偶数(2, 4, 6, 8, 10)が出力されます。
応用例3: 大規模データセットの効率的処理
遅延評価の特性を持つカスタムシーケンスは、大規模データセットを扱う際に特に有用です。たとえば、数百万行のデータを持つファイルを読み込むシーケンスを実装し、必要なときだけ行を一つずつ処理することができます。
struct FileLineSequence: Sequence {
let filePath: String
func makeIterator() -> FileLineIterator? {
return FileLineIterator(filePath: filePath)
}
}
struct FileLineIterator: IteratorProtocol {
let fileHandle: FileHandle
let encoding: String.Encoding
var lineReader: LineReader
init?(filePath: String, encoding: String.Encoding = .utf8) {
guard let fileHandle = FileHandle(forReadingAtPath: filePath) else {
return nil
}
self.fileHandle = fileHandle
self.encoding = encoding
self.lineReader = LineReader(fileHandle: fileHandle, encoding: encoding)
}
mutating func next() -> String? {
return lineReader.nextLine()
}
}
このシーケンスは、非常に大きなファイルから1行ずつ遅延評価でデータを読み込み、メモリ効率の良い処理が可能です。
シーケンスを活用することで、さまざまなデータ処理の場面で柔軟なアプローチが可能になります。特に、カスタムシーケンスは効率的なデータ生成、フィルタリング、大規模データの処理など、多くの分野で応用が可能です。
パフォーマンスに配慮したシーケンス設計
シーケンスの実装において、パフォーマンスを最適化することは非常に重要です。特に、大規模なデータセットや繰り返し処理を扱う場合、シーケンスの設計がシステム全体のパフォーマンスに大きな影響を与えることがあります。このセクションでは、パフォーマンスに配慮したシーケンス設計のポイントを解説します。
遅延評価 (Lazy Evaluation)
遅延評価は、必要なときにだけデータを生成または取得する方法です。これにより、大量のデータをメモリに全て読み込む必要がなくなり、メモリ効率が向上します。Swiftでは、標準のシーケンスメソッドを遅延評価にするためのlazy
プロパティが用意されています。例えば、フィルタリングやマッピングなどの処理を遅延評価にすることで、計算コストを抑えることができます。
let lazySequence = (1...1000000).lazy.filter { $0 % 2 == 0 }
このコードでは、フィルタリングされた結果がすぐには計算されず、データが実際に必要になるまで計算が遅延されます。
メモリ効率の向上
シーケンスの設計において、メモリの使用量を最小限に抑えることも重要です。特に、メモリに大量のデータを保持せず、必要な部分だけを処理する設計が有効です。上記の遅延評価と併せて、データを小分けに処理するか、ストリーム処理を活用する方法が有効です。例えば、ファイルやネットワークからのデータ取得において、全データを一度に取得するのではなく、部分的に読み込むことでメモリ使用量を削減できます。
イテレーションの最適化
シーケンスのイテレーションが頻繁に発生する場合、その処理を最適化することでパフォーマンスが向上します。例えば、次の要素を計算する処理が重い場合、それをキャッシュすることが考えられます。また、アルゴリズム自体を効率化し、不要な再計算を避けることも有効です。
struct OptimizedIterator: IteratorProtocol {
var current = 0
var end = 1000000
mutating func next() -> Int? {
guard current < end else { return nil }
current += 2 // 偶数だけを計算
return current
}
}
このように、無駄な計算を避けて効率よく次の要素を取得する設計が、パフォーマンスを大きく向上させます。
並列処理の利用
シーケンス処理を並列化することで、特にマルチコアCPUを活用する環境ではパフォーマンスが向上します。Swiftでは、DispatchQueue
やOperationQueue
を使って並列処理を容易に実装できます。例えば、大量のデータに対して複数のスレッドで同時に処理を行うことが可能です。
let queue = DispatchQueue.global(qos: .userInitiated)
queue.async {
for number in 1...100000 {
print(number)
}
}
ただし、並列処理を使う際には、データの競合や同期に注意が必要です。
計算量の削減
シーケンスの設計時には、アルゴリズムの計算量も重要な要素です。特に、大規模なデータセットを扱う場合、計算の複雑さがパフォーマンスに大きな影響を与えます。例えば、線形時間(O(n))で済む処理を、誤って二次時間(O(n^2))のアルゴリズムで実装してしまうと、大量のデータを扱ったときに著しく処理時間が増加します。効率的なアルゴリズムを選択し、計算量を最小限に抑えることが重要です。
まとめ
シーケンスのパフォーマンスに配慮することで、特に大規模なデータや高頻度のイテレーションを扱う際にシステムの効率が向上します。遅延評価を利用したメモリ効率化や、イテレーターの最適化、並列処理の活用など、設計段階でこれらの要素を考慮することで、Swiftのシーケンスを効果的に活用できます。
シーケンスを拡張する方法
Swiftのシーケンスは強力な機能を持っていますが、独自のシーケンスをさらに機能的にするために拡張を行うことができます。拡張を行うことで、既存のシーケンスに新しいメソッドやプロパティを追加し、特定の目的に適した操作を追加できます。このセクションでは、シーケンスの拡張方法とその活用例について解説します。
シーケンス拡張の基本
Swiftでは、extension
キーワードを使用して既存の型に対して新しい機能を追加できます。これにより、標準のシーケンス型や自作のシーケンス型に新しいメソッドやプロパティを柔軟に追加できます。例えば、シーケンスの要素を逆順に返すカスタムメソッドを追加することが可能です。
extension Sequence {
func reversedSequence() -> [Element] {
return self.reversed()
}
}
この例では、どんなシーケンスにもreversedSequence
メソッドを追加し、そのシーケンスの要素を逆順にした配列を返す機能を提供しています。
カスタムシーケンスへの拡張
自作のカスタムシーケンスにも同じように拡張を適用できます。例えば、カスタムシーケンスの要素をさらに加工するメソッドや、要素の集計を行うメソッドを追加することが考えられます。次に、数値シーケンスに対して、全ての要素を2倍にするメソッドを追加してみましょう。
extension Sequence where Element == Int {
func doubledElements() -> [Int] {
return self.map { $0 * 2 }
}
}
この拡張は、整数型のシーケンスに対してのみ動作し、全ての要素を2倍にした配列を返します。
let numbers = [1, 2, 3, 4, 5]
let doubledNumbers = numbers.doubledElements()
print(doubledNumbers) // [2, 4, 6, 8, 10]
パフォーマンスに配慮した拡張
拡張メソッドを作成する際には、パフォーマンスへの配慮も必要です。例えば、大規模なデータセットに対して多くの操作を繰り返す場合、メモリ効率や遅延評価を考慮した設計が重要です。以下の例では、lazy
を使って遅延評価を適用したフィルタリングメソッドを追加します。
extension Sequence where Element: Comparable {
func filterGreaterThan(_ value: Element) -> [Element] {
return self.lazy.filter { $0 > value }.map { $0 }
}
}
この拡張は、指定した値より大きい要素をフィルタリングし、それらを返しますが、lazy
を使って必要なときだけフィルタリング処理が行われるようにしています。
let largeNumbers = numbers.filterGreaterThan(3)
print(largeNumbers) // [4, 5]
シーケンスのカスタム操作
他にも、シーケンスの集約や複雑な条件でのフィルタリング、カスタムの高階関数を追加することも可能です。例えば、特定の条件に合致する要素を複数回繰り返して返すシーケンスを作成することもできます。
extension Sequence {
func repeatElements(times: Int) -> [Element] {
return self.flatMap { Array(repeating: $0, count: times) }
}
}
このメソッドを使うと、シーケンスの全ての要素を指定した回数だけ繰り返した新しいシーケンスを取得できます。
let repeatedNumbers = numbers.repeatElements(times: 3)
print(repeatedNumbers) // [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5]
まとめ
シーケンスを拡張することで、より機能的で柔軟なデータ操作が可能になります。必要に応じたメソッドやプロパティを追加することで、コードの再利用性や読みやすさが向上します。また、拡張時にはパフォーマンスやメモリ効率に配慮し、最適な設計を行うことが大切です。
テストとデバッグ
カスタムシーケンスやイテレーターを実装した後は、その動作が期待通りであるかを確認するために、テストとデバッグが欠かせません。テストを通じてシーケンスが正しく機能していることを確認し、デバッグによってバグやパフォーマンスの問題を見つけることができます。このセクションでは、テストとデバッグの具体的な方法について解説します。
ユニットテストでの確認
Swiftでは、XCTestフレームワークを使用してユニットテストを行うことができます。カスタムシーケンスのテストでは、makeIterator
メソッドやnext()
メソッドが正しく動作しているかを確認します。以下に、カスタムシーケンスのユニットテストの例を示します。
import XCTest
@testable import YourProject
class SequenceTests: XCTestCase {
func testCounterSequence() {
let counter = Counter(start: 1, end: 5)
var iterator = counter.makeIterator()
XCTAssertEqual(iterator.next(), 1)
XCTAssertEqual(iterator.next(), 2)
XCTAssertEqual(iterator.next(), 3)
XCTAssertEqual(iterator.next(), 4)
XCTAssertEqual(iterator.next(), 5)
XCTAssertNil(iterator.next())
}
func testEvenNumberSequence() {
let evenNumbers = EvenNumberSequence(start: 1, end: 10)
var iterator = evenNumbers.makeIterator()
XCTAssertEqual(iterator.next(), 2)
XCTAssertEqual(iterator.next(), 4)
XCTAssertEqual(iterator.next(), 6)
XCTAssertEqual(iterator.next(), 8)
XCTAssertEqual(iterator.next(), 10)
XCTAssertNil(iterator.next())
}
}
このテストでは、Counter
とEvenNumberSequence
が想定通りに動作するかどうかを確認しています。XCTAssertEqual
を使って、次の要素が正しく返されていることをテストし、シーケンスが終了したらnil
を返すかどうかもチェックしています。
デバッグのポイント
デバッグは、コードが期待通りに動作しているかを確認し、問題が発生した場合にその原因を特定するための重要なステップです。Swiftのデバッグには、Xcodeのデバッガーやprint()
関数を使って簡単に行うことができます。例えば、イテレーターの進行状況を確認したい場合、next()
メソッド内でprint()
を使って現在の状態を出力することができます。
struct CounterIterator: IteratorProtocol {
var current: Int
let end: Int
mutating func next() -> Int? {
guard current <= end else {
return nil
}
print("Returning \(current)") // デバッグのために状態を出力
let nextValue = current
current += 1
return nextValue
}
}
これにより、next()
がどのように呼び出され、次にどの値が返されるかをデバッグ中に確認できます。
パフォーマンスのテスト
パフォーマンスが重要なカスタムシーケンスでは、パフォーマンステストも行うべきです。XCTestには、パフォーマンスを測定するためのmeasure
メソッドが用意されています。これを使って、シーケンスやイテレーターが大規模データセットに対して効率的に動作するかを確認できます。
func testPerformanceExample() {
self.measure {
let largeSequence = Counter(start: 1, end: 1000000)
_ = largeSequence.map { $0 * 2 }
}
}
このテストでは、100万個の要素を持つカスタムシーケンスのパフォーマンスを測定し、必要に応じて最適化を検討します。
バグ修正とリファクタリング
デバッグを通じて発見されたバグを修正し、その後のコードのメンテナンスをしやすくするために、リファクタリングも重要です。例えば、繰り返し発生するパターンや複雑なロジックを簡素化することで、将来のバグを防ぐことができます。
よくあるバグと対策
- 無限ループ:イテレーターが適切に終了しない場合、無限ループが発生します。
nil
を正しく返してイテレーションを終了するように注意します。 - メモリリーク:特に遅延評価を使う場合、不要なデータがメモリに残らないように設計を見直します。リソースの適切な管理が重要です。
まとめ
テストとデバッグは、カスタムシーケンスが正しく動作し、パフォーマンス面でも問題がないことを確認するための不可欠なプロセスです。ユニットテストを通じて機能の正確性を保証し、デバッグで動作を確認しながら、必要に応じてパフォーマンステストを行い、最適なシーケンスを実現します。
練習問題
カスタムシーケンスやイテレーターの実装方法を理解するために、以下の練習問題に取り組んでみましょう。これらの問題を解くことで、SwiftのSequence
プロトコルに対する理解が深まり、実際にカスタムシーケンスを作成できるようになります。
問題1: フィボナッチ数列シーケンスの実装
フィボナッチ数列とは、最初の2つの数が1であり、次の数が前の2つの数の合計で定義される数列です。このフィボナッチ数列を返すカスタムシーケンスFibonacciSequence
を実装してください。n
番目までのフィボナッチ数を生成するイテレーターを作成し、シーケンスとして使用できるようにします。
struct FibonacciSequence: Sequence {
let n: Int
func makeIterator() -> FibonacciIterator {
return FibonacciIterator(n: n)
}
}
struct FibonacciIterator: IteratorProtocol {
var current = 0
var nextValue = 1
var count = 0
let n: Int
mutating func next() -> Int? {
guard count < n else {
return nil
}
let result = current
current = nextValue
nextValue = result + nextValue
count += 1
return result
}
}
課題: FibonacciSequence
を使って、最初の10個のフィボナッチ数を出力してください。
問題2: 累乗シーケンスの作成
指定した基数と指数の範囲に基づいて累乗数を返すシーケンスを実装してください。たとえば、base
が2で、startPower
が1、endPower
が5の場合、2^1
, 2^2
, 2^3
, 2^4
, 2^5
が出力されます。PowerSequence
という名前でシーケンスを作成し、base
とstartPower
、endPower
を初期化する構造体を実装してください。
struct PowerSequence: Sequence {
let base: Int
let startPower: Int
let endPower: Int
func makeIterator() -> PowerIterator {
return PowerIterator(base: base, currentPower: startPower, endPower: endPower)
}
}
struct PowerIterator: IteratorProtocol {
let base: Int
var currentPower: Int
let endPower: Int
mutating func next() -> Int? {
guard currentPower <= endPower else {
return nil
}
let result = Int(pow(Double(base), Double(currentPower)))
currentPower += 1
return result
}
}
課題: PowerSequence
を使用して、2の1乗から5乗までを順に出力してください。
問題3: ランダム数シーケンスに範囲フィルターを追加
以前に紹介したRandomNumberSequence
に範囲フィルタリング機能を追加してください。このフィルタリング機能では、指定した最小値と最大値の範囲に収まるランダムな数だけを返すようにします。シーケンスの拡張機能を利用して、新しいフィルタリングメソッドを実装してください。
extension RandomNumberSequence {
func filterInRange(min: Int, max: Int) -> [Int] {
return self.filter { $0 >= min && $0 <= max }
}
}
課題: 1から100の範囲でランダムに生成された数値をフィルタリングし、50から80の範囲に収まる数値のみを出力してください。
まとめ
これらの練習問題を通じて、カスタムシーケンスの設計やイテレーターの実装を練習できます。シーケンスを使ってデータを効率的に扱う方法を習得することで、Swiftでのデータ操作がさらに強力になります。
まとめ
本記事では、Swiftにおける「Sequence」プロトコルの基本的な概念から、独自のシーケンスとイテレーターの実装方法までを詳しく解説しました。シーケンスの遅延評価やパフォーマンス最適化、さらに機能拡張によって、効率的で柔軟なデータ処理が可能になります。カスタムシーケンスは、特定のニーズに合わせたデータ処理をシンプルかつ効果的に実現する強力なツールです。
コメント