لیستها، مجموعهها و نقشهها (Lists, Sets, Maps)
در آموزش کلاسهای داده، Collectionها ساختارهایی هستند که برای ذخیره و مدیریت دادهها در قالبهای مختلف استفاده میشوند. سه Collection اصلی در کاتلین شامل *List*، *Set* و *Map* هستند. این مجموعهها به دو نوع *read-only* (فقط خواندنی) و *mutable* (قابل تغییر) تقسیم میشوند و میتوان آنها را از یک نوع به نوع دیگر تغییر داد.
توضیح انواع Collection در کاتلین
– لیست (List): مجموعهای مرتب که ترتیب عناصر را حفظ میکند و اجازهٔ تکرار عناصر را میدهد.
– مجموعه (Set): مجموعهای که ترتیب خاصی ندارد و از تکرار عناصر جلوگیری میکند.
– مپ (Map): مجموعهای از جفتهای کلید-مقدار (*Key-Value*) که هر کلید به یک مقدار یکتا مرتبط میشود.
ساختار read-only و mutable
read-only: فقط خواندنی و بدون امکان تغییر.
mutable: امکان تغییر در عناصر و اضافه یا حذف آنها را فراهم میکند.
1. لیست (List)
در کاتلین، میتوانید از `listOf()` برای ایجاد لیست فقط خواندنی و از `mutableListOf()` یا `toMutableList()` برای لیست تغییرپذیر استفاده کنید.
fun main() {
// لیست فقط خواندنی
val readOnlyList = listOf("apple", "banana", "cherry")
// تبدیل به لیست تغییرپذیر
val mutableList = readOnlyList.toMutableList()
mutableList.add("date")
println("Read-Only List: $readOnlyList")
println("Mutable List: $mutableList")
}
2. مجموعه (Set)
در کاتلین، `setOf()` یک مجموعه فقط خواندنی و `mutableSetOf()` یا `toMutableSet()` یک مجموعه تغییرپذیر میسازد.
fun main() {
// مجموعه فقط خواندنی
val readOnlySet = setOf("apple", "banana", "cherry")
// تبدیل به مجموعه تغییرپذیر
val mutableSet = readOnlySet.toMutableSet()
mutableSet.add("date")
println("Read-Only Set: $readOnlySet")
println("Mutable Set: $mutableSet")
}
3. مپ (Map)
در کاتلین، `mapOf()` یک مپ فقط خواندنی و `mutableMapOf()` یا `toMutableMap()` یک مپ تغییرپذیر میسازد.
fun main() {
// مپ فقط خواندنی
val readOnlyMap = mapOf(1 to "apple", 2 to "banana", 3 to "cherry")
// تبدیل به مپ تغییرپذیر
val mutableMap = readOnlyMap.toMutableMap()
mutableMap[4] = "date"
println("Read-Only Map: $readOnlyMap")
println("Mutable Map: $mutableMap")
}
ساختار درختی Collections در کاتلین
ساختار درختی زیر نشان میدهد که چگونه هر Collection میتواند به دو نوع فقط خواندنی و قابل تغییر تقسیم شود.
+-------------------------+
| Collection |
+-------------------------+
/ | \
/ | \
+-------+ +-------+ +-------+
| List | | Set | | Map |
+-------+ +-------+ +-------+
| | |
+------------------+------------------+
| | |
+---------------+ +---------------+ +---------------+
| read-only | | read-only | | read-only |
+---------------+ +---------------+ +---------------+
| | |
+---------------+ +---------------+ +---------------+
| mutable | | mutable | | mutable |
+---------------+ +---------------+ +---------------+
این ساختار درختی به طور خلاصه نشان میدهد که هر Collection در کاتلین میتواند به دو نوع *read-only* و *mutable* تقسیم شود و هر نوع قابلیت تبدیل به دیگری را دارد.
توابع کار با Collectionها (Filter, Map, Reduce, etc)
در کاتلین، توابعی که بهعنوان Member-Function داخل کلاسها تعریف میشوند، قابلیتهای زیادی برای کار با Collection ها دارند. کاتلین توابعی همچون `filter`، `map`، و `reduce` را فراهم کرده که به ما کمک میکنند تا بهسادگی دادههای درون Collectionها را پردازش و تجزیه و تحلیل کنیم. این توابع بهطور گسترده برای پردازش دادهها، اعمال تغییرات، و فیلتر کردن یا محاسبه مقادیر استفاده میشوند.
1. تابع `filter`
تابع `filter` لیستی جدید ایجاد میکند که فقط شامل عناصری است که شرط خاصی را برآورده میکنند. این تابع، یک **لامبدا** (lambda) بهعنوان پارامتر میگیرد و در صورت صدق شرط، عنصر را به لیست جدید اضافه میکند.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6)
// فیلتر کردن اعداد زوج
val evenNumbers = numbers.filter { it % 2 == 0 }
println("Original List: $numbers")
println("Filtered List (Even Numbers): $evenNumbers")
}
خروجی:
Original List: [1, 2, 3, 4, 5, 6] Filtered List (Even Numbers): [2, 4, 6]
2. تابع `map`
تابع `map` برای اعمال یک تابع روی هر عنصر مجموعه استفاده میشود و نتیجه را به یک لیست جدید بازمیگرداند. این تابع نیز یک لامبدا میگیرد و بهازای هر عنصر، تغییرات موردنظر را اعمال میکند.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// ضرب هر عنصر در 2
val doubledNumbers = numbers.map { it * 2 }
println("Original List: $numbers")
println("Mapped List (Doubled Numbers): $doubledNumbers")
}
خروجی:
Original List: [1, 2, 3, 4, 5] Mapped List (Doubled Numbers): [2, 4, 6, 8, 10]
3. تابع `reduce`
تابع `reduce` تمام عناصر یک Collection را به یک مقدار تبدیل میکند، مثلاً برای جمع یا ضرب همهٔ عناصر. این تابع یک **عملگر دودویی** میگیرد که دو عنصر را پردازش کرده و آنها را به یک مقدار تبدیل میکند.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// جمع تمامی عناصر
val sum = numbers.reduce { acc, number -> acc + number }
println("Original List: $numbers")
println("Sum of Elements: $sum")
}
خروجی:
Original List: [1, 2, 3, 4, 5] Sum of Elements: 15
4. تابع `forEach`
تابع `forEach` برای اعمال یک عملیات روی هر عنصر از مجموعه استفاده میشود. این تابع مقداری بازنمیگرداند و برای انجام عملیاتهای بدون نتیجهٔ بازگشتی مناسب است.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println("Elements in the List:")
numbers.forEach { println(it) }
}
خروجی:
Elements in the List: 1 2 3 4 5
5. تابع `find`
تابع `find` اولین عنصری را که شرط خاصی را برآورده میکند، برمیگرداند. اگر هیچ عنصری پیدا نشود، مقدار `null` بازمیگرداند.
fun main() {
val names = listOf("Alice", "Bob", "Charlie", "David")
// پیدا کردن اولین اسمی که با حرف "C" شروع میشود
val nameStartingWithC = names.find { it.startsWith("C") }
println("Name starting with 'C': $nameStartingWithC")
}
خروجی:
Name starting with 'C': Charlie
6. تابع `groupBy`
تابع `groupBy` عناصر را بر اساس یک کلید خاص گروهبندی کرده و بهعنوان یک Map بازمیگرداند که در آن، کلیدها نتیجهی تابع لامبدا هستند و مقادیر لیست عناصری هستند که به آن کلید تعلق دارند.
fun main() {
val names = listOf("Alice", "Bob", "Charlie", "David", "Daniel")
// گروهبندی اسامی بر اساس اولین حرف
val groupedNames = names.groupBy { it.first() }
println("Grouped Names: $groupedNames")
}
خروجی:
Grouped Names: {A=[Alice], B=[Bob], C=[Charlie], D=[David, Daniel]}
7. ترکیب توابع
میتوان توابع را با هم ترکیب کرد تا پردازشهای پیچیدهتری انجام داد.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// پیدا کردن مجموع اعداد زوج که در دو ضرب شدهاند
val sumOfDoubledEvenNumbers = numbers
.filter { it % 2 == 0 }
.map { it * 2 }
.reduce { acc, number -> acc + number }
println("Sum of Doubled Even Numbers: $sumOfDoubledEvenNumbers")
}
خروجی:
Sum of Doubled Even Numbers: 60
این توابع (مثل `filter`، `map` و `reduce`) ابزاری قدرتمند برای کار با Collectionها در کاتلین هستند و ترکیب آنها میتواند پردازشهای پیچیدهتری را بهسادگی ممکن کند.
قابلیتهای پیشرفته مانند Lazy Sequences
در کاتلین، علاوه بر `lazy` برای مقداردهی تنبل یک متغیر، میتوان از سکانسهای تنبل (Lazy Sequences) برای پردازش دادهها بهصورت تنبل و بازدهی بهتر استفاده کرد. این قابلیت کمک میکند تا مجموعههای بزرگ داده به شکلی بهینه و با مصرف حافظه کمتر پردازش شوند، زیرا عناصر یک سکانس تنها زمانی محاسبه یا ایجاد میشوند که واقعاً به آنها نیاز باشد.
1. مفهوم Lazy Sequences در کاتلین
Lazy Sequence یک سکانس است که بهصورت تنبل پردازش میشود. این به این معناست که هر عملگر روی یک سکانس تنبل تنها زمانی اجرا میشود که به نتیجه نهایی دسترسی پیدا کنیم. این ویژگی بهویژه در مجموعههای داده بزرگ کاربرد دارد، زیرا در این حالت، پردازش روی دادهها تنها تا حد نیاز انجام میشود.
ایجاد یک Lazy Sequence
برای ساختن یک سکانس تنبل در کاتلین، از تابع `sequence` یا تبدیل یک مجموعه با تابع `asSequence()` استفاده میکنیم.
2. ساخت سکانس با `sequence`
میتوانیم با استفاده از تابع `sequence`، یک سکانس سفارشی تنبل ایجاد کنیم. این تابع از یک لامبدا برای تولید عناصر استفاده میکند و تنها به تعداد لازم عناصر را ایجاد میکند.
مثال: ایجاد سکانسی از اعداد فرد به صورت تنبل
fun main() {
val oddNumbers = sequence {
var n = 1
while (true) {
yield(n) // مقدار جدیدی تولید میکند
n += 2
}
}
// دریافت 5 عدد فرد اول
println(oddNumbers.take(5).toList())
}
خروجی:
[1, 3, 5, 7, 9]
در این مثال، سکانسی از اعداد فرد ایجاد کردهایم که بهصورت تنبل تولید میشود و تنها ۵ عدد اول را پردازش و نمایش میدهد.
3. تبدیل یک Collection به Lazy Sequence با `asSequence()`
میتوانیم با استفاده از `asSequence()`، یک مجموعه را به سکانسی تنبل تبدیل کنیم. این ویژگی برای بهینهسازی پردازش دادهها کاربرد دارد، چون پردازشها تا حد نیاز انجام میشوند و از پردازشهای غیرضروری جلوگیری میکند.
مثال: پردازش یک لیست از اعداد با Sequence
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6)
val processedNumbers = numbers
.asSequence()
.map { it * 2 } // ضرب در ۲
.filter { it > 5 } // فیلتر عناصر بزرگتر از ۵
.toList() // تبدیل به لیست نهایی
println("Processed Numbers: $processedNumbers")
}
خروجی:
Processed Numbers: [6, 8, 10, 12]
در این مثال، ابتدا با `asSequence()` لیست به یک سکانس تبدیل میشود. سپس پردازشها به ترتیب روی هر عنصر اعمال میشود و تنها عناصری که مورد نیاز هستند، پردازش میشوند. با `toList()` نتیجه به لیست تبدیل میشود.
4. تفاوت Sequence و Collection در پردازش
Collectionها (مانند `List` و `Set`) بهصورت فوری پردازش میشوند، به این معنا که هر عملگر روی تمامی عناصر اعمال میشود. ولی در **Sequence**ها، پردازش بهصورت تنبل و عنصر به عنصر انجام میشود.
**مثال برای درک تفاوت:**
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println("Using Collection:")
list.map {
println("Map: $it")
it * 2
}.filter {
println("Filter: $it")
it > 5
}
println("\nUsing Sequence:")
list.asSequence()
.map {
println("Map: $it")
it * 2
}
.filter {
println("Filter: $it")
it > 5
}
.toList()
}
خروجی:
Using Collection: Map: 1 Map: 2 Map: 3 Map: 4 Map: 5 Filter: 2 Filter: 4 Filter: 6 Filter: 8 Filter: 10 Using Sequence: Map: 1 Filter: 2 Map: 2 Filter: 4 Map: 3 Filter: 6 Map: 4 Filter: 8 Map: 5 Filter: 10
در اینجا، در Collection ابتدا تمامی عناصر `map` میشوند و سپس `filter` اعمال میشود. اما در Sequence پردازش بهصورت تنبل انجام میشود و برای هر عنصر، عملگرها به ترتیب و بهصورت همزمان اعمال میشوند.
5. مثال کاربردی با استفاده از سکانسهای تنبل
فرض کنید لیستی از اسامی داریم و میخواهیم اسامی بلندتر از ۵ کاراکتر را انتخاب کرده و آنها را به حروف بزرگ تبدیل کنیم، اما پردازش بهصورت تنبل انجام شود.
fun main() {
val names = listOf("Alice", "Bob", "Catherine", "Daniel", "Eva")
val longNames = names
.asSequence()
.filter { it.length > 5 }
.map { it.uppercase() }
.toList()
println("Long Names in Uppercase: $longNames")
}
خروجی:
Long Names in Uppercase: [CATHERINE, DANIEL]
در این مثال، `filter` و `map` بهصورت تنبل روی لیست اعمال میشوند و تنها دو اسم که شرط را برآورده میکنند، پردازش میشوند.
مزایا و نکات مهم استفاده از Lazy Sequences
1. افزایش کارایی: با استفاده از Lazy Sequence میتوان از پردازشهای غیرضروری جلوگیری کرد. این ویژگی بهویژه زمانی مفید است که با مجموعههای داده بزرگ کار میکنیم.
2. کاهش مصرف حافظه: چون دادهها تنها زمانی پردازش میشوند که مورد نیاز باشند، مصرف حافظه کاهش مییابد.
3. تبدیل سریع به Collection: در انتهای پردازش، با استفاده از `toList()`، `toSet()` و … میتوانید نتیجه را به نوعی دیگر از Collection تبدیل کنید.
4. همگامسازی با سایر توابع مجموعهای: Lazy Sequenceها را میتوان به راحتی با توابع دیگر مانند `map`، `filter`، `reduce` و غیره ترکیب کرد.
در مجموع، Lazy Sequenceها برای پردازشهای سنگین و مجموعههای بزرگ بسیار مفید هستند و کمک میکنند برنامهها بهینهتر و با کارایی بالاتر اجرا شوند.
