1. Giris
Siz satri “hisselere” bolmeye baslayanda birden goreceksiniz ki, satr dost paketlenmis biskvit kimi deyil, daha cox qapagı bezen olmayan biskvit qutusuna oxsayir. Istifadeci bos satr daxil ede biler, ayiricini unuda biler, ard-arda iki bosluq qoya biler ve ya umumen nese yaradiciliqla yaza biler. Bu leksiyada kodu ele yazmagi oyrenirik ki, o coxmesin, eksine, gozlenilen sekilde davranis strategiyasini secsin.
Tesavvur edin ki, proqraminiz kassirdir, istifadeci ise alicidir ve o bezen aydin danismir. Kassir “eee… he… corek… yegin ki” kimi ifadeden huşunu itirmemelidir. O ya yeniden sorusmalidir, ya da susmaya gore qebul olunmus qerar vermelidir. Satrlarin parslanmasinda da eyni sey: biz evvelceden layihalendiririk ki, “tapmadıq”, “sigmadıq”, “bosdur” hallari olanda ne edek.
2. Satrda indeksler ve serhedler
Kotlin-de satr — indeksleri olan simvollarin ardicilligidir. Indeksleme sifirdan baslayir. Bu tanis gelir, amma klassik sehvler mehz burada yaranir: “niye length indeksine gore simvol goturmek olmur?” ve “niye lastIndex — length deyil?”.
Bu fikre oyresmek vacibdir: length simvollarin sayidir, son icaze verilen indeks ise — length - 1-dir (eger satr bos deyilse).
Bunu “sehrli qayda” kimi yaddasda saxlamamaq ucun bele dusunmek rahatdir: length — “son simvoldan sonrakı movqe”dir. O, “sona qeder kesmek” kimi emeliyyatlar ucun faydalidir, amma simvol indeksi kimi yararli deyil.
Satri kesende zehnen tesavvur etmeye deyer kicik bir sxem:
Stroka: "Kotlin"
Indeksy: 012345
length = 6
lastIndex = 5
Kodda mini-yoxlama (beyin goze inansin deye):
fun main() {
val s = "Kotlin"
println(s.length) // 6
println(s.lastIndex) // 5
println(s[0]) // K
println(s[s.lastIndex]) // n
}
Eger satr bosdursa (""), onda length == 0, lastIndex ise == -1. Bu bug deyil, duzgun xeberdarliqdir: “son element yoxdur”.
3. indexOf(...): -1 — “tapilmadi” cavabidir
Satrda ayirici axtaranda (meselen, '=', '@', ':') ilk addim kimi cox vaxt indexOf-dan istifade edirik. Burada Kotlin-in (umumilikde dilllerin coxunun) felsefesini qebul etmek vacibdir: element tapilmayanda size -1 qaytarilir. Proqram coxmur. Bu sadəcə bele demeyin yoludur: “axtardim, amma tapmadim”.
Ona gore de yeni baslayanlarin tipik sehvi bele olur: “indeksi tapdim, derhal substring etdim, her sey coxdu”. Coxmesinin sebebi odur ki, indeks -1 idi, substring(-1, …) ise movcud olmayan movqeden metn kesmeye cehddir.
Email ile sade bir numune:
fun main() {
val email = "alice@example.com"
val at = email.indexOf('@')
val domain = if (at >= 0) email.substring(at + 1) else "INVALID"
println(domain) // example.com
}
Menasi email-de deyil (poct oz heyatini yasayir), menasi patterndedir: indexOf → yoxlama → sonra kesmek.
4. substring(...): serhedler ve “end daxil edilmir”
substring — guclu bicagdir. Her bicag kimi, o da yalniz kor-korane sallamadiginiz halda yaxsi isleyir.
En vacib qayda:
- substring(start, end) start indeksinden daxil olmaqla end indeksine qeder daxil olmamaqla simvollari goturur.
Demeli, end — “dayanma movqeyi”dir, “son simvol” deyil.
Gelin, qisa bir numune ile mohkemledirek:
fun main() {
val s = "Kotlin"
println(s.substring(0, 1)) // K
println(s.substring(0, 2)) // Ko
println(s.substring(1, 4)) // otl
}
Bu niye rahatdir? Cunki end-i qorxmadan length-e beraber qoya bilersiniz. Bu, “son simvoldan sonrakı movqe”dir ve kesme serhedi kimi duzgundur.
Yəni bele — normaldir:
fun main() {
val s = "Kotlin"
println(s.substring(0, s.length)) // Kotlin
}
Amma bele — artiq serhed sehvidir:
// s.substring(0, s.length + 1) // bele etmek olmaz: serheddi asariq
substring(start) — “quyruq”u goturmek usulu kimi
Bezen end gostermeye ehtiyac olmur, cunki “satrin sonuna qeder” ele lazim olan seydir. Bunun ucun substring(start) formasi var.
Ayiricini artiq tapmisinizsa ve ondan sonrakinin hamisini goturmek isteyirsinizse, bunu istifade etmek rahatdir:
fun main() {
val path = "/users/alice"
val slash = path.indexOf('/')
val tail = if (slash >= 0) path.substring(slash + 1) else path
println(tail) // users/alice
}
Beli, numune qesden “qeribe”dir (cunki '/' burada evvelde durur). Amma fikri gosterir: substring(start) length ve “end daxil edilmir” haqqinda dusunme ehtiyacini aradan qaldirir. Siz sadəcə deyirsiniz: “buradan baslayib sona qeder hamisini ver”.
5. Pattern: ayiricini tapdiq, yoxladiq, kesdik
Indi leksiyanin esas islek patternini yigiriq. Bu her yerde lazimdir: key=value tipli konfiqlar, add:100:food tipli komandalar, log satrlari, “ad: deyer” ve s.
Insan dili ile alqoritm:
- Ayiricini tapiriq (indexOf).
- Tapilmayibsa — davranisi secirik (null qaytarmaq, defolt qaytarmaq, butun satri “acar” saymaq ve s.).
- Tapilibsa — sol ve sag hisseni diqqetle kesirik.
“acar=deyer” numunesi:
fun main() {
val line = "key=value"
val eq = line.indexOf('=')
val key = if (eq >= 0) line.substring(0, eq) else line
val value = if (eq >= 0) line.substring(eq + 1) else ""
println("key=$key, value=$value") // key=key, value=value
}
Diqqet edin: biz bir tapilmis indeksle (eq) her iki hisseni gotururuk. Bu hem daha qisa, hem de daha tehlukesizdir: ele olmayacaq ki, “acar”i bir qayda ile, “deyer”i basqa qayda ile kesek.
6. Daxilolmanin hazirlanmasi: trim() ve tehlukesiz split
Istifadeci daxilolmasi demek olar ki, hemise “cirklidir”: artiq bosluqlar, tablar, tesadufi setirsonlari, sonda bosluq, evvelde bosluq. Ve siz satri hazirlamadan parslamağa baslasaniz, “her sey duz kimidir, amma niye acar bosdur?” kimi qeribe problemlere dusersiniz.
Ona gore esas intizam bele gorunur: evvel trim(), sonra analiz.
fun main() {
val raw = " key=value "
val line = raw.trim()
val eq = line.indexOf('=')
val key = if (eq >= 0) line.substring(0, eq) else line
val value = if (eq >= 0) line.substring(eq + 1) else ""
println("key='$key', value='$value'") // key='key', value='value'
}
Burada cixisda dirnaq isaretleri — sirf pedoqoji fendidır: bosluq qalarsa, gozle gormeye komek edir.
split(" ") ve coxsayli bosluqlar
Satri “sozlere” bolmek isteyende cox vaxt split(" ") edirik. Selis daxilolmada bu isleyir. Amma "one two three" kimi daxilolmada birden neticelerin arasinda bos satrlar alirsiniz. Cunki split(" ") sozun esl menasinda tek bosluqlara gore kesir, ardicil bosluqlar olanda ise aralarinda “bos soz” qalir.
Ona gore, emrlerin ve daxilolmanin parslanmasinda praktik olaraq hemise uc addim faydalidir: trim() → split(" ") → filter { it.isNotEmpty() }.
fun main() {
val raw = " one two three "
val parts = raw.trim()
.split(" ")
.filter { it.isNotEmpty() }
println(parts) // [one, two, three]
}
Bu patterni tez-tez goreceksiniz, cunki o, “insan daxilolmasi”ni “komputer tokenleri”ne cevirir. Oxsar uslubda strukturlesdirilmis daxilolma oxuma funksiyalari da yazilir.
7. Sehvde strategiyalar: marker vs null
Indi vacib dizayn meselesi: satr “uygun deyilse” ne etmeliyik?
Iki cox yasanan yanaşma var.
- Birinci yanaşma — marker qaytarmaq (meselen, "INVALID" ve ya bos satr) ve davam etmek. Hesabat hazirlayirsinizsa ve “hec olmasa nese gosterim” isteyirsinizse rahat ola biler. Amma tehlukelidir: markeri emal etmeyi asanliqla unudursunuz ve onda "INVALID" birdən-bire normal deyer kimi irəli keçir.
- Ikinci yanaşma — null qaytarmaq ve çağıran kodu qerar vermeye mecbur etmek. Bu uslub adeten daha etibarlidir: netice olmaya da biler — bunu aydin gorursunuz. Nullable tiplərlə islemeyi artiq bacaririq, bu gun de əsas alətimiz bu olacaq.
Gelin, tehsil ucun konsol tetbiqimize (sertli xerc trekeri) balaca parser-funksiya elave edek: “key=value tokenini parse et”.
Funksiya parseKeyValue(token: String): Pair<String, String>?
Bu funksiya hec ne cap etmir, hec ne oxumur; sadəcə satri diqqetle parse edir ve ya cut (pair), ya da null qaytarir. Bele uslubu test etmek ve yeniden istifade etmek asandir.
fun parseKeyValue(token: String): Pair<String, String>? {
val eq = token.indexOf('=')
if (eq < 0) return null
val key = token.substring(0, eq).trim()
val value = token.substring(eq + 1).trim()
if (key.isEmpty()) return null
return key to value
}
“Tehlukesizlik noqte”lerine diqqet:
- eq < 0 → derhal null, kesmeye cehd etmirik.
- substring(eq + 1) yalniz ona gore tehlukesizdir ki, eq >= 0. eq == token.lastIndex olanda deyer bos olacaq, amma bu, coxme deyil.
- key.isEmpty() — "=123" kimi hallardan qoruma.
Mini-yoxlama:
fun main() {
println(parseKeyValue("amount=120")) // (amount, 120)
println(parseKeyValue("=120")) // null
println(parseKeyValue("amount=")) // (amount, )
println(parseKeyValue("amount")) // null
}
Komandanin pars edilmesi: “oxudu → hazirladi → parse etdi”
Indi gelin, xerc ucotu tetbiqimiz ucun kicik (amma realist) konsol interfeysi hissesi yigag.
Biz isteyirik ki, istifadeci komandalari bele daxil etsin:
- add amount=120 category=food note=coffee
- add amount=999 category=tech note=keyboard
Bu gun formatlasdirmani, saxlamani, hesabatlari ve s. murəkkeblesdirmirik. Məqsədimiz — eyri daxilolmada coxmemek ve komanda niye parse olunmadigini aydin anlamaqdir.
Evvel daxilolmani temizleyirik ve tokenlere boluruk:
fun splitTokens(line: String): List<String> {
return line.trim()
.split(" ")
.filter { it.isNotEmpty() }
}
Yoxlama:
fun main() {
val tokens = splitTokens(" add amount=120 category=food ")
println(tokens) // [add, amount=120, category=food]
}
Sonra komandanı ve arqumentleri tehlukesiz gotururuk. Vacib: tokens[0] siyahi bos olanda tehlukelidir. Ona gore de evvel isEmpty() yoxlayiriq.
fun main() {
val line = readln().trim()
val tokens = splitTokens(line)
if (tokens.isEmpty()) {
println("Bos komanda. Nese daxil edin.")
return
}
val command = tokens[0].lowercase()
val args = tokens.drop(1)
println("command=$command, args=$args")
}
readln() funksiyasini konsol proqramlarinda esas daxilolma kimi istifade edirik.
key=value arqumentlerini Map-a parse edirik
Indi novbeti addimi edirik: add komandasinindan sonra gelen tokenler siyahisini emal edek ve amount, category, note deyerlerini cixarmağa calisaq.
Tokenler uzerinden kecib yalniz etibarli key=value-lerden Map<String, String> yigən funksiya yaziriq. Map ile tanisiq ve bilirik ki, map[key] null qaytara biler; demeli, bu da tehlukesiz proqramlasdirma stilimize uyğundur.
fun parseArgs(tokens: List<String>): Map<String, String> {
val result = mutableMapOf<String, String>()
for (t in tokens) {
val kv = parseKeyValue(t) ?: continue
val (k, v) = kv
result[k] = v
}
return result
}
Yoxlama:
fun main() {
val args = parseArgs(listOf("amount=120", "category=food", "oops", "note=coffee"))
println(args) // {amount=120, category=food, note=coffee}
}
Gorun, biz strategiya secdik: token “pis”dirse ("oops"), onu kecirik. Sehv qaytara da bilerdik — amma bu artiq basqa strategiyadir. Bu gun bize davamlıliq ve sadelik vacibdir.
Final mini-senarI: add komandasi ve diaqnostik mesajlar
Indi her seyi birlesdirek: setri oxuyuruq, komandanı gotururuk, arqumentleri parse edirik, mecburi sahelerin olub-olmadigini yoxlayiriq.
Indi tam xerc saxlanmasini reallasdirmiriq (bu artiq diger leksiyalarda olub/olacaq). Sadəcə gosteririk ki, parse isleyir ve coxmur.
fun main() {
val tokens = splitTokens(readln())
if (tokens.isEmpty()) {
println("Komanda daxil edin, meselen: add amount=120 category=food note=coffee")
return
}
val command = tokens[0].lowercase()
val args = parseArgs(tokens.drop(1))
if (command != "add") {
println("Naməlum komanda: $command")
return
}
val amountRaw = args["amount"]
val category = args["category"]
val note = args["note"] ?: ""
val amount = amountRaw?.toIntOrNull()
if (amount == null || category == null || category.isBlank()) {
println("Yanlis daxilolma. Lazimdir: amount=<Int> ve category=<text>")
return
}
println("Xerci elave edirik: amount=$amount, category=$category, note=$note")
}
Burada dərhal bir nece “tehlukesizlik ruhunda” vacib meqam var:
- Biz komandanin mutleq oldugunu ferz etmirik.
- Biz amount ve category-nin mutleq verildiyini ferz etmirik.
- Biz amount-un eded oldugunu ferz etmirik ( toIntOrNull() istifade edirik).
- Indekslere gore kesmeyi yoxlamasiz etmirik, cunki bunu parseKeyValue-a cixarmisiq.
8. Satr parslanmasinda tipik sehvler
Sehv №1: indexOf neticesinden sanki hemise duzgunmus kimi istifade etmek.
Cox vaxt bu bele gorunur: val i = s.indexOf('='); val left = s.substring(0, i). “Duzgun” setirlerde her sey isleyir, amma '=' olmayan setirde istisna alirsiniz. Mualicesi bir vərdişdir: indexOf her zaman yoxlanilir, cunki -1 — “tapilmadi” ucun standart cavabdir.
Sehv №2: substring(start, end) serhedlerini qarisdirmaq ve end-in daxil edilmesini gozlemek.
Bu klassikdir, cunki beyin “daxil olmaqla” rejimini sevir. Kotlin-de end daxil edilmir. Eger “ayiriciya qeder” kesirsinizse, substring(0, eq) artiq '=' simvolunu daxil etmir ve bu, elə rahatdir. Amma “son simvolu da daxil edim” cəhdi tez-tez end = length + 1 kimi sehvə ve coxmeye getirir.
Sehv №3: bos satri kesmek ve orada hec olmasa bir simvol var sanmaq.
Bu, yoxlamasiz substring(0, 1) kimi kodda gorunur. Bos daxilolmada bu qezadir. Davamli kodda ya isNotEmpty() yoxlayirsiniz, ya da logikanı ele qurursunuz ki, bos satr ayrica budaqda emal olunsun (meselen, “yeniden daxil edin”).
Sehv №4: split(" ") etmek ve “artiq bosluqlar mane olmayacaq” deye inanmaq.
Mane olur. Coxsayli bosluqlar bos tokenler yaradir, sonra onlar arqument parslanmasini gozlenilmez sekilde pozur. “trim() → split(" ") → filter { it.isNotEmpty() }” patterni davranisi proqnozlasdirilan edir ve “bezen isleyir, bezen yox” kimi gorunen qeribe baglari aradan qaldirir.
Sehv №5: sehvde strategiya secmek yerine “ne bahasina olursa-olsun kesmeye” calismaq.
Bezen yazmaq isteyirsiniz: “istifadeci duzgun daxil etmelidir”. Amma tecrube eksini gosterir: istifadeci nece alinarsa, ele de daxil edir. Ona gore evvelceden qerar vermek lazimdir: sehvde null qaytaririq, defolt qaytaririq, yoxsa diaqnostik mesaj cap edib yeniden daxil etmesini xahis edirik. Esas odur ki, davranis asqar olsun, “oz-ozune bele alindi” olmasin.
GO TO FULL VERSION