В настоящее время я преподаю курс прикладного анализа текста и использую Scala в качестве языка программирования, который преподается и используется в курсе. Вместо того, чтобы создавать больше учебников, я решил взять страницу из пьесы Брайана Даннинга в его подкасте « Скептоид» (очень рекомендуется), когда он отвечает на вопросы студентов. Итак, я попросил студентов курса представить вопросы о Скале, которые у них были, основываясь на чтениях и заданиях, которые были до сих пор. Этот пост охватывает более половины из них — остальное будет рассмотрено в последующем посте.
Я начну с некоторых более простых вопросов, и вопросы и / или ответы постепенно переходят к темам более среднего уровня. Предложения и комментарии по улучшению любого из ответов очень приветствуются!
Основные вопросы
Q. Относительно адресации частей переменных: Для адресации отдельных частей списков нумерация элементов равна (Список 0,1,2 и т. Д.), То есть первый элемент называется «0». Похоже, то же самое для массивов и карт, но не для кортежей — чтобы получить первый элемент кортежа, мне нужно использовать Tuple._1. Почему это?
О. Это просто вопрос соглашения — кортежи использовали индекс на основе 1 в других языках, таких как Haskell, и кажется, что Scala приняла ту же конвенцию / традицию. Увидеть:
http://stackoverflow.com/questions/6241464/why-are-the-indexes-of-scala-tuples-1-based
В. Кажется, что Scala не распознает граничный символ «b» как регулярное выражение. Есть ли что-то подобное в Scala?
А. Скала распознает граничные символы. Например, следующий сеанс REPL объявляет регулярное выражение, которое находит «the» с границами, и успешно извлекает три токена «the» в примере предложения.
1
2
3
4
5
6
7
8
|
scala> val TheRE = "" "\bthe\b" "" .r TheRE : scala.util.matching.Regex = \bthe\b scala> val sentence = "She think the man is a stick-in-the-mud, but the man disagrees." sentence : java.lang.String = She think the man is a stick-in-the-mud, but the man disagrees. scala> TheRE.findAllIn(sentence).toList res 1 : List[String] = List(the, the, the) |
В. Почему метод «split» не работает с аргументами? Пример: val arg = args.split (»«). Аргументы правильные, так что split должен работать?
О. Переменная args является массивом, поэтому split на них не работает. По сути, массивы уже разделены.
В. Какова основная разница между foo.mapValues (x => x.length) и foo.map (x => x.length) . В некоторых местах один работает, а другой нет.
О. Функция map работает со всеми типами последовательностей, включая Seqs и Maps (обратите внимание, что Maps можно рассматривать как последовательности Tuple2s). Однако функция mapValues работает только на картах. По сути, это удобная функция. Как пример, давайте начнем с простой карты от Ints до Ints.
1
2
|
scala> val foo = List(( 1 , 2 ),( 3 , 4 )).toMap foo : scala.collection.immutable.Map[Int,Int] = Map( 1 -> 2 , 3 -> 4 ) |
Теперь рассмотрим задачу добавления 2 к каждому значению на карте. Это можно сделать с помощью функции map следующим образом.
1
2
|
scala> foo.map { case (key,value) = > (key,value+ 2 ) } res 5 : scala.collection.immutable.Map[Int,Int] = Map( 1 -> 4 , 3 -> 6 ) |
Итак, функция map выполняет итерации по парам ключ / значение. Нам нужно сопоставить их обоих, а затем вывести ключ и измененное значение, чтобы создать новую карту. Функция mapValues делает это немного проще.
1
2
|
scala> foo.mapValues( 2 +) res 6 : scala.collection.immutable.Map[Int,Int] = Map( 1 -> 4 , 3 -> 6 ) |
Возвращаясь к вопросу о вычислении длины с использованием mapValues или map — тогда это просто вопрос о том, какие значения вы преобразуете, как в следующих примерах.
01
02
03
04
05
06
07
08
09
10
11
|
scala> val sentence = "here is a sentence with some words" .split( " " ).toList sentence : List[java.lang.String] = List(here, is, a, sentence, with , some, words) scala> sentence.map( _ .length) res 7 : List[Int] = List( 4 , 2 , 1 , 8 , 4 , 4 , 5 ) scala> val firstCharTokens = sentence.groupBy(x = >x( 0 )) firstCharTokens : scala.collection.immutable.Map[Char,List[java.lang.String]] = Map(s -> List(sentence, some), a -> List(a), i -> List(is), h -> List(here), w -> List( with , words)) scala> firstCharTokens.mapValues( _ .length) res 9 : scala.collection.immutable.Map[Char,Int] = Map(s -> 2 , a -> 1 , i -> 1 , h -> 1 , w -> 2 ) |
В. Есть ли какая-либо функция, которая разбивает список на два списка с элементами в чередующихся позициях исходного списка? Например,
MainList = (1,2,3,4,5,6)
List1 = (1,3,5)
List2 = (2,4,6)
О. Учитывая точный основной список, который вы предоставили, можно использовать функцию разбиения и использовать операцию по модулю, чтобы увидеть, делится ли значение равномерно на 2 или нет.
1
2
3
4
5
|
scala> val mainList = List( 1 , 2 , 3 , 4 , 5 , 6 ) mainList : List[Int] = List( 1 , 2 , 3 , 4 , 5 , 6 ) scala> mainList.partition( _ % 2 == 0 ) res 0 : (List[Int], List[Int]) = (List( 2 , 4 , 6 ),List( 1 , 3 , 5 )) |
Итак, раздел возвращает пару списков. Первый имеет все элементы, которые соответствуют условию, а второй имеет все те, которые не соответствуют.
Конечно, это не сработает в общем случае для списков, в которых есть строки, или в которых нет Ints по порядку и т. Д. Однако индексы списка всегда хорошо себя ведут таким образом, поэтому нам просто нужно сделать немного больше работы, сжав каждый элемент с его индексом, а затем разделив его на основе индексов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
scala> val unordered = List( "b" , "2" , "a" , "4" , "z" , "8" ) unordered : List[java.lang.String] = List(b, 2 , a, 4 , z, 8 ) scala> unordered.zipWithIndex res 1 : List[(java.lang.String, Int)] = List((b, 0 ), ( 2 , 1 ), (a, 2 ), ( 4 , 3 ), (z, 4 ), ( 8 , 5 )) scala> val (evens, odds) = unordered.zipWithIndex.partition( _ . _ 2 % 2 == 0 ) evens : List[(java.lang.String, Int)] = List((b, 0 ), (a, 2 ), (z, 4 )) odds : List[(java.lang.String, Int)] = List(( 2 , 1 ), ( 4 , 3 ), ( 8 , 5 )) scala> evens.map( _ . _ 1 ) res 2 : List[java.lang.String] = List(b, a, z) scala> odds.map( _ . _ 1 ) res 3 : List[java.lang.String] = List( 2 , 4 , 8 ) |
Исходя из этого, вы, конечно, могли бы написать функцию, которая делает это для любого произвольного списка.
В. Как преобразовать Список в Вектор и наоборот?
A. Используйте toIndexSeq и toList .
01
02
03
04
05
06
07
08
09
10
11
|
scala> val foo = List( 1 , 2 , 3 , 4 ) foo : List[Int] = List( 1 , 2 , 3 , 4 ) scala> val bar = foo.toIndexedSeq bar : scala.collection.immutable.IndexedSeq[Int] = Vector( 1 , 2 , 3 , 4 ) scala> val baz = bar.toList baz : List[Int] = List( 1 , 2 , 3 , 4 ) scala> foo == baz res 0 : Boolean = true |
Q. Преимущество вектора над списком — поиск в постоянном времени. В чем преимущество использования списка над вектором?
О. Список немного быстрее для операций в начале (спереди) последовательности, поэтому, если все, что вы делаете, это делает обход (доступ к каждому элементу по порядку, например, при отображении), то списки вполне адекватны и могут быть более эффективный. У них также есть хорошее поведение для сопоставления с образцом для операторов case.
Тем не менее, общепринятым кажется, что вы должны по умолчанию использовать Векторы. Посмотрите хороший ответ Даниэля Спивака на Stackoverflow:
http://stackoverflow.com/questions/6928327/when-should-i-choose-vector-in-scala
В. При разбиении строк holmes.split («\\ s») — \ n и \ t просто требуется один «\» для распознавания его специальной функциональности, но почему два символа «\» требуются для символа пробела?
О. Это потому, что \ n и \ t действительно что-то значат в строке.
1
2
3
4
5
6
7
|
scala> println( "Here is a line with a tab\tor\ttwo, followed by\na new line." ) Here is a line with a tab or two, followed by a new line. scala> println( "This will break\s." ) <console> : 1 : error : invalid escape character println( "This will break\s." ) |
Итак, вы предоставляете аргумент String для split, и он использует его для создания регулярного выражения. Учитывая, что \ s не является строковым символом, но является метасимволом регулярных выражений, вам нужно избегать его. Конечно, вы можете использовать split («» \ s »») , но это не совсем лучше в этом случае.
В. Я давно программирую на C ++ и Java. Поэтому я бессознательно ставлю точку с запятой в конце строки. Кажется, что стандартный стиль кодирования Scala не рекомендует использовать точки с запятой. Тем не менее, я видел, что в некоторых случаях требуются точки с запятой, как вы показали в прошлом классе. Есть ли какая-то конкретная причина, по которой точка с запятой теряет свою роль в Scala?
О. Основная причина заключается в улучшении читабельности, поскольку точка с запятой редко требуется при написании стандартного кода в редакторах (в отличие от однострочников в REPL). Однако, если вы хотите сделать что-то в одной строке, например, обрабатывать несколько случаев, вам нужны точки с запятой.
1
2
3
4
5
|
scala> val foo = List( "a" , 1 , "b" , 2 ) foo : List[Any] = List(a, 1 , b, 2 ) scala> foo.map { case (x : String) = > x; case (x : Int) = > x.toString } res 5 : List[String] = List(a, 1 , b, 2 ) |
Но, в общем, лучше всего разбить эти случаи на несколько строк в любом реальном коде.
В. Нет ли способа использовать _ в map-подобных методах для коллекций, состоящих из пар? Например, List ((1,1), (2,2)). Map (e => e._1 + e._2) работает, но List ((1,1), (2,2)). Map (_._ 1 + _._ 2) не работает.
A. Область, в которой _ остается не однозначным, заканчивается после первого вызова, поэтому вы можете использовать его только один раз. В любом случае, лучше использовать оператор case, который проясняет, каковы члены пар.
1
2
|
scala> List(( 1 , 1 ),( 2 , 2 )).map { case (num 1 , num 2 ) = > num 1 +num 2 } res 6 : List[Int] = List( 2 , 4 ) |
В. Я не уверен в точном значении и разнице между «=>» и «->». Кажется, они оба означают что-то вроде «применить X к Y», и я вижу, что каждый из них используется в определенном контексте, но какая логика стоит за этим?
О. Использование -> просто создает Tuple2, как довольно ясно из следующего фрагмента.
1
2
3
4
5
6
7
8
|
scala> val foo = ( 1 , 2 ) foo : (Int, Int) = ( 1 , 2 ) scala> val bar = 1 -> 2 bar : (Int, Int) = ( 1 , 2 ) scala> foo == bar res 11 : Boolean = true |
Прежде всего, это синтаксический сахар, который обеспечивает интуитивно понятный символ для создания элементов карты. Сравните следующие два способа объявления одной и той же карты.
1
2
3
4
5
|
scala> Map(( "a" , 1 ),( "b" , 2 )) res 9 : scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1 , b -> 2 ) scala> Map( "a" -> 1 , "b" -> 2 ) res 10 : scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1 , b -> 2 ) |
Второе мне кажется более читабельным.
Использование => указывает, что вы определяете функцию. Основная форма АРГУМЕНТЫ => РЕЗУЛЬТАТ.
01
02
03
04
05
06
07
08
09
10
11
|
scala> val addOne = (x : Int) = > x+ 1 addOne : Int = > Int = <function 1 > scala> addOne( 2 ) res 7 : Int = 3 scala> val addTwoNumbers = (num 1 : Int, num 2 : Int) = > num 1 +num 2 addTwoNumbers : (Int, Int) = > Int = <function 2 > scala> addTwoNumbers( 3 , 5 ) res 8 : Int = 8 |
Обычно вы используете его при определении анонимных функций в качестве аргументов таких функций, как map , filter и тому подобное.
В. Есть ли более удобный способ выражения гласных как [AEIOUaeiou] и согласных как [BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz] в RegExes?
О. Вы можете использовать строки при определении регулярных выражений, поэтому у вас может быть переменная для гласных и одна для согласных.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
scala> val vowel = "[AEIOUaeiou]" vowel : java.lang.String = [AEIOUaeiou] scala> val consonant = "[BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz]" consonant : java.lang.String = [BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz] scala> val MyRE = ( "(" +vowel+ ")(" +consonant+ ")(" +vowel+ ")" ).r MyRE : scala.util.matching.Regex = ([AEIOUaeiou])([BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz])([AEIOUaeiou]) scala> val MyRE(x,y,z) = "aJE" x : String = a y : String = J z : String = E |
Q. «\ b» в RegExes обозначает границу, верно? Таким образом, он также фиксирует «-». Но если у меня есть одна строка «sdnfeorgn», она НЕ охватывает границы этого, это правильно? И если так, то почему бы и нет?
О. Потому что в этой строке нет границ!
Промежуточные вопросы
В. Функция flatMap берет списки списков и объединяет их в один список. Но в примере:
1
2
3
4
5
|
scala> ( 1 to 10 ).toList.map(x = >squareOddNumber(x)) res 16 : List[Option[Int]] = List(Some( 1 ), None, Some( 9 ), None, Some( 25 ), None, Some( 49 ), None, Some( 81 ), None) scala> ( 1 to 10 ).toList.flatMap(x = >squareOddNumber(x)) res 17 : List[Int] = List( 1 , 9 , 25 , 49 , 81 ) |
Здесь это не список списка, а просто список. В этом случае ожидается, что этот список будет списком параметров.
Я попытался запустить код с функцией, возвращающей только число или None. Это показало ошибку. Так есть ли способ использовать flatmap без списков параметров и просто список. Например, List (1, None, 9, None, 25) должен быть возвращен как List (1, 9, 25) .
О. Нет, это не будет работать, потому что List (1, None, 9, None, 25) смешивает параметры с Ints .
1
2
|
scala> val mixedup = List( 1 , None, 9 , None, 25 ) mixedup : List[Any] = List( 1 , None, 9 , None, 25 ) |
Итак, ваша функция должна возвращать Option, что означает возвращение Somes или Nones . Тогда flatMap будет работать счастливо.
Один из способов думать о Options заключается в том, что они похожи на списки с нулем или одним элементом, что можно заметить по параллелям в следующем фрагменте.
01
02
03
04
05
06
07
08
09
10
11
|
scala> val foo = List(List( 1 ),Nil,List( 3 ),List( 6 ),Nil) foo : List[List[Int]] = List(List( 1 ), List(), List( 3 ), List( 6 ), List()) scala> foo.flatten res 12 : List[Int] = List( 1 , 3 , 6 ) scala> val bar = List(Option( 1 ),None,Option( 3 ),Option( 6 ),None) bar : List[Option[Int]] = List(Some( 1 ), None, Some( 3 ), Some( 6 ), None) scala> bar.flatten res 13 : List[Int] = List( 1 , 3 , 6 ) |
В. Есть ли в scala универсальные шаблоны (например, C ++, Java)? например. в C ++ мы можем использовать vector <int>, vector <string> и т. д. Возможно ли это в scala? Если так, то как?
О. Да, каждый тип коллекции параметризован. Обратите внимание, что каждая из следующих переменных параметризована типом элементов, с которыми они инициализируются.
1
2
3
4
5
6
7
8
|
scala> val foo = List( 1 , 2 , 3 ) foo : List[Int] = List( 1 , 2 , 3 ) scala> val bar = List( "a" , "b" , "c" ) bar : List[java.lang.String] = List(a, b, c) scala> val baz = List( true , false , true ) baz : List[Boolean] = List( true , false , true ) |
Вы можете создавать свои собственные параметризованные классы напрямую.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
scala> class Flexible[T] ( val data : T) defined class Flexible scala> val foo = new Flexible( 1 ) foo : Flexible[Int] = Flexible @ 7 cd 0570 e scala> val bar = new Flexible( "a" ) bar : Flexible[java.lang.String] = Flexible @ 31 b 6956 f scala> val baz = new Flexible( true ) baz : Flexible[Boolean] = Flexible @ 5 b 58539 f scala> foo.data res 0 : Int = 1 scala> bar.data res 1 : java.lang.String = a scala> baz.data res 2 : Boolean = true |
В. Как мы можем легко создавать, инициализировать и работать с многомерными массивами (и словарями)?
A. Используйте функцию заполнения объекта Array для их создания.
1
2
3
4
5
6
7
8
|
scala> Array.fill( 2 )( 1.0 ) res 8 : Array[Double] = Array( 1.0 , 1.0 ) scala> Array.fill( 2 , 3 )( 1.0 ) res 9 : Array[Array[Double]] = Array(Array( 1.0 , 1.0 , 1.0 ), Array( 1.0 , 1.0 , 1.0 )) scala> Array.fill( 2 , 3 , 2 )( 1.0 ) res 10 : Array[Array[Array[Double]]] = Array(Array(Array( 1.0 , 1.0 ), Array( 1.0 , 1.0 ), Array( 1.0 , 1.0 )), Array(Array( 1.0 , 1.0 ), Array( 1.0 , 1.0 ), Array( 1.0 , 1.0 ))) |
После того, как они у вас есть, вы можете перебирать их как обычно.
1
2
3
4
5
|
scala> val my 2 d = Array.fill( 2 , 3 )( 1.0 ) my 2 d : Array[Array[Double]] = Array(Array( 1.0 , 1.0 , 1.0 ), Array( 1.0 , 1.0 , 1.0 )) scala> my 2 d.map(row = > row.map(x = >x+ 1 )) res 11 : Array[Array[Double]] = Array(Array( 2.0 , 2.0 , 2.0 ), Array( 2.0 , 2.0 , 2.0 )) |
Для словарей (Карт) вы можете использовать изменяемые HashMaps, чтобы создать пустую Карту, а затем добавить к ней элементы. Для этого смотрите этот пост в блоге:
http://bcomposes.wordpress.com/2011/09/19/first-steps-in-scala-for-beginning-programmers-part-8/
В. Является ли функция apply похожа на конструктор в C ++, Java? Где будет применяться функция apply ? Это для инициализации значений атрибутов?
О. Нет, функция apply похожа на любую другую функцию, за исключением того, что она позволяет вам вызывать ее без выписывания «apply». Рассмотрим следующий класс.
1
2
3
4
|
class AddX (x : Int) { def apply(y : Int) = x+y override def toString = "My number is " + x } |
Вот как мы можем это использовать.
01
02
03
04
05
06
07
08
09
10
11
|
scala> val add 1 = new AddX( 1 ) add 1 : AddX = My number is 1 scala> add 1 ( 4 ) res 0 : Int = 5 scala> add 1 .apply( 4 ) res 1 : Int = 5 scala> add 1 .toString res 2 : java.lang.String = My number is 1 |
Таким образом, метод apply является просто (очень удобным) синтаксическим сахаром, который позволяет вам указать одну функцию как основную для класса, который вы разработали (на самом деле, вы можете иметь несколько методов apply, если каждый из них имеет уникальный список параметров). Например, для списков метод apply возвращает значение по указанному индексу, а для Maps — значение, связанное с данным ключом.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
scala> val foo = List( 1 , 2 , 3 ) foo : List[Int] = List( 1 , 2 , 3 ) scala> foo( 2 ) res 3 : Int = 3 scala> foo.apply( 2 ) res 4 : Int = 3 scala> val bar = Map( 1 -> 2 , 3 -> 4 ) bar : scala.collection.immutable.Map[Int,Int] = Map( 1 -> 2 , 3 -> 4 ) scala> bar( 1 ) res 5 : Int = 2 scala> bar.apply( 1 ) res 6 : Int = 2 |
В. В учебном пособии SBT вы обсуждаете «Узел» и «Значение» как классы дел. Какая альтернатива кейсу?
А. Нормальный класс. Классы случая — особый случай. Они делают две вещи (и больше) для вас. Во-первых, вам не нужно использовать «новый» для создания нового объекта. Рассмотрим следующие идентичные классы.
01
02
03
04
05
06
07
08
09
10
11
|
scala> class NotACaseClass ( val data : Int) defined class NotACaseClass scala> case class IsACaseClass ( val data : Int) defined class IsACaseClass scala> val foo = new NotACaseClass( 4 ) foo : NotACaseClass = NotACaseClass @ a 5 c 0 f 8 f scala> val bar = IsACaseClass( 4 ) bar : IsACaseClass = IsACaseClass( 4 ) |
Это может показаться незначительным, но может значительно улучшить читаемость кода. Например, рассмотрите возможность создания списков в списках внутри списков, если вам приходилось все время использовать «новый». Это определенно верно для Node и Value , которые используются для построения деревьев.
Классы case также поддерживают сопоставление, как показано ниже.
1
2
|
scala> val IsACaseClass(x) = bar x : Int = 4 |
Нормальный класс не может этого сделать.
1
2
3
4
5
6
7
|
scala> val NotACaseClass(x) = foo <console> : 13 : error : not found : value NotACaseClass val NotACaseClass(x) = foo ^ <console> : 13 : error : recursive value x needs type val NotACaseClass(x) = foo ^ |
Если вы смешаете класс case в List и отобразите его, вы можете сопоставить его с другими классами, такими как Lists и Ints. Рассмотрим следующий гетерогенный список.
1
2
|
scala> val stuff = List(IsACaseClass( 3 ), List( 2 , 3 ), IsACaseClass( 5 ), 4 ) stuff : List[Any] = List(IsACaseClass( 3 ), List( 2 , 3 ), IsACaseClass( 5 ), 4 ) |
Мы можем преобразовать это в список Ints путем обработки каждого элемента в соответствии с его типом путем сопоставления.
1
2
3
4
5
6
7
8
9
|
scala> stuff.map { case List(x,y) = > x; case IsACaseClass(x) = > x; case x : Int = > x } <console> : 13 : warning : match is not exhaustive! missing combination * Nil * * stuff.map { case List(x,y) = > x; case IsACaseClass(x) = > x; case x : Int = > x } ^ warning : there were 1 unchecked warnings; re-run with -unchecked for details res 10 : List[Any] = List( 3 , 2 , 5 , 4 ) |
Если вы не хотите видеть предупреждение в REPL, добавьте регистр для вещей, которые не совпадают, которые выдают MatchError.
1
2
3
|
scala> stuff.map { case List(x,y) = > x; case IsACaseClass(x) = > x; case x : Int = > x; case _ = > throw new MatchError } warning : there were 1 unchecked warnings; re-run with -unchecked for details res 13 : List[Any] = List( 3 , 2 , 5 , 4 ) |
А еще лучше, вернуть Options (используя None для несопоставленного случая) и flatMapping вместо этого.
1
2
3
|
scala> stuff.flatMap { case List(x,y) = > Some(x); case IsACaseClass(x) = > Some(x); case x : Int = > Some(x); case _ = > None } warning : there were 1 unchecked warnings; re-run with -unchecked for details res 14 : List[Any] = List( 3 , 2 , 5 , 4 ) |
Q. В C ++ спецификатор доступа по умолчанию является приватным; в Java нужно указать private или public для каждого члена класса, где, как и в Scala, спецификатор доступа по умолчанию для класса — public. Какова может быть мотивация дизайна, когда одна из целей класса — скрытие данных?
О. Причина в том, что Scala имеет гораздо более совершенную схему спецификации доступа, чем Java, что делает публичный рациональный выбор. Смотрите обсуждение здесь:
http://stackoverflow.com/questions/4656698/default-public-access-in-scala
Другим ключевым аспектом этого является то, что в Scala основной упор делается на использование неизменяемых структур данных, поэтому нет никакой опасности того, что кто-то изменит внутреннее состояние ваших объектов, если вы спроектировали их таким образом. Это, в свою очередь, избавляет от нелепых методов получения и установки, которые размножаются и размножаются в программах Java. См. «Почему добытчики и сеттеры злые» для дальнейшего обсуждения:
http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
После того, как вы привыкнете к программированию в Scala, все, что так часто встречается в Java-коде для getter / setter, в значительной степени достойно кляпов.
В общем, все еще хорошая идея использовать private [this] в качестве модификатора для методов и переменных, когда они нужны только самому объекту.
В. Как мы определяем перегруженные конструкторы в Scala?
Q. То, как класс определяется в Scala, введенном в руководстве, похоже, имеет только один конструктор. Есть ли способ предоставить несколько конструкторов, таких как Java?
О. Вы можете добавить дополнительные конструкторы с этими объявлениями.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class SimpleTriple (x : Int, y : Int, z : String) { def this (x : Int, z : String) = this (x, 0 ,z) def this (x : Int, y : Int) = this (x,y, "a" ) override def toString = x + ":" + y + ":" + z } scala> val foo = new SimpleTriple( 1 , 2 , "hello" ) foo : SimpleTriple = 1 : 2 : hello scala> val bar = new SimpleTriple( 1 , "goodbye" ) bar : SimpleTriple = 1 : 0 : goodbye scala> val baz = new SimpleTriple( 1 , 3 ) baz : SimpleTriple = 1 : 3 : a |
Обратите внимание, что вы должны указать начальное значение для каждого из параметров класса. Это контрастирует с Java, который позволяет оставить некоторые поля неинициализированными (что приводит к неприятным ошибкам и плохому дизайну).
Обратите внимание, что вы также можете указать параметры по умолчанию.
1
2
3
4
5
6
7
8
9
|
class SimpleTripleWithDefaults (x : Int, y : Int = 0 , z : String = "a" ) { override def toString = x + ":" + y + ":" + z } scala> val foo = new SimpleTripleWithDefaults( 1 ) foo : SimpleTripleWithDefaults = 1 : 0 : a scala> val bar = new SimpleTripleWithDefaults( 1 , 2 ) bar : SimpleTripleWithDefaults = 1 : 2 : a |
Однако вы не можете опустить средний параметр при указании последнего.
1
2
3
4
5
6
7
|
scala> val foo = new SimpleTripleWithDefaults( 1 , "xyz" ) <console> : 12 : error : type mismatch; found : java.lang.String( "xyz" ) required : Int Error occurred in an application involving default arguments. val foo = new SimpleTripleWithDefaults( 1 , "xyz" ) ^ |
Но вы можете назвать параметры при инициализации, если хотите это сделать.
1
2
|
scala> val foo = new SimpleTripleWithDefaults( 1 ,z = "xyz" ) foo : SimpleTripleWithDefaults = 1 : 0 : xyz |
Тогда у вас будет полная свобода изменять параметры.
1
2
|
scala> val foo = new SimpleTripleWithDefaults(z = "xyz" ,x = 42 ,y = 3 ) foo : SimpleTripleWithDefaults = 42 : 3 : xyz |
Q. Я до сих пор не понимаю разницу между классами и чертами. Думаю, я вижу концептуальную разницу, но я не совсем понимаю, в чем заключается функциональная разница — как создание «черты» отличается от создания класса, возможно, с меньшим количеством методов, связанных с ним?
О. Да, они разные. Во-первых, черты являются абстрактными, что означает, что вы не можете создавать никаких членов. Рассмотрим следующий контраст.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
scala> class FooClass defined class FooClass scala> trait FooTrait defined trait FooTrait scala> val fclass = new FooClass fclass : FooClass = FooClass @ 1 b 499616 scala> val ftrait = new FooTrait <console> : 8 : error : trait FooTrait is abstract ; cannot be instantiated val ftrait = new FooTrait ^ |
Вы можете расширить черту, чтобы сделать конкретный класс, однако.
1
2
3
4
5
|
scala> class FooTraitExtender extends FooTrait defined class FooTraitExtender scala> val ftraitExtender = new FooTraitExtender ftraitExtender : FooTraitExtender = FooTraitExtender @ 53 d 26552 |
Это становится более интересным, если у черты есть несколько методов, конечно. Вот черта Animal , которая объявляет два абстрактных метода, makeNoise и doBehavior .
1
2
3
4
|
trait Animal { def makeNoise : String def doBehavior (other : Animal) : String } |
Мы можем расширить эту черту новыми определениями классов; каждый расширяющий класс должен реализовывать оба этих метода (или быть объявленным абстрактным).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
case class Bear (name : String, defaultBehavior : String = "Regard warily..." ) extends Animal { def makeNoise = "ROAR!" def doBehavior (other : Animal) = other match { case b : Bear = > makeNoise + " I'm " + name + "." case m : Mouse = > "Eat it!" case _ = > defaultBehavior } override def toString = name } case class Mouse (name : String) extends Animal { def makeNoise = "Squeak?" def doBehavior (other : Animal) = other match { case b : Bear = > "Run!!!" case m : Mouse = > makeNoise + " I'm " + name + "." case _ = > "Hide!" } override def toString = name } |
Обратите внимание, что Bear и Mouse имеют разные списки параметров, но оба могут быть животными, потому что они полностью реализуют черту Animal. Теперь мы можем начать создавать объекты классов Bear и Mouse и заставить их взаимодействовать. Нам не нужно использовать «new», потому что они являются классами case (и это также позволило использовать их в выражениях match методов doBehavior ).
1
2
3
4
5
6
7
8
|
val yogi = Bear( "Yogi" , "Hello!" ) val baloo = Bear( "Baloo" , "Yawn..." ) val grizzly = Bear( "Grizzly" ) val stuart = Mouse( "Stuart" ) println(yogi + ": " + yogi.makeNoise) println(stuart + ": " + stuart.makeNoise) println( "Grizzly to Stuart: " + grizzly.doBehavior(stuart)) |
Мы также можем создать одноэлементный объект типа Animal с помощью следующего объявления.
1
2
3
4
5
6
7
8
|
object John extends Animal { def makeNoise = "Hullo!" def doBehavior (other : Animal) = other match { case b : Bear = > "Nice bear... nice bear..." case _ = > makeNoise } override def toString = "John" } |
Здесь Джон — это объект, а не класс. Поскольку этот объект реализует черту Animal , он успешно расширяет ее и может действовать как Animal . Это означает, что медведь, подобный балу, может взаимодействовать с Джоном .
1
|
println( "Baloo to John: " + baloo.doBehavior(John)) |
Вывод приведенного выше кода при запуске в виде сценария следующий.
Стюарт: Писк?
Гризли Стюарту: Ешь!
Балу Джону: зевать …
Более близкое различие между чертами и абстрактными классами. Фактически, все показанное выше могло бы быть сделано с Animal как абстрактный класс, а не как черта. Одно из отличий состоит в том, что абстрактный класс может иметь конструктор, а черты — нет. Другое ключевое отличие между ними заключается в том, что признаки могут использоваться для поддержки ограниченного множественного наследования, как показано в следующем вопросе / ответе.
В. Поддерживает ли Scala множественное наследование?
О. Да, через черты с реализациями некоторых методов. Вот пример с признаком Clickable, который имеет абстрактный (не реализованный) метод getMessage , реализованный метод click и частную переназначаемую переменную numTimesClicked (последние два ясно показывают, что признаки отличаются от интерфейсов Java).
1
2
3
4
5
6
7
8
9
|
trait Clickable { private var numTimesClicked = 0 def getMessage : String def click = { val output = numTimesClicked + ": " + getMessage numTimesClicked + = 1 output } } |
Теперь давайте предположим, что у нас есть класс MessageBearer (который мы, возможно, хотели по совершенно другим причинам, не имеющим отношения к щелчку).
1
2
3
|
class MessageBearer ( val message : String) { override def toString = message } |
Теперь можно создать новый класс, расширив MessageBearer и добавив в него черту Clickable .
1
2
3
|
class ClickableMessageBearer(message : String) extends MessageBearer(message) with Clickable { def getMessage = message } |
ClickableMessageBearer теперь обладает способностями как MessageBearers (которая должна быть в состоянии получить его сообщение), так и Clickables .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
scala> val cmb 1 = new ClickableMessageBearer( "I'm number one!" ) cmb 1 : ClickableMessageBearer = I'm number one! scala> val cmb 2 = new ClickableMessageBearer( "I'm number two!" ) cmb 2 : ClickableMessageBearer = I'm number two! scala> cmb 1 .click res 3 : java.lang.String = 0 : I 'm number one! scala> cmb1.message res4: String = I' m number one! scala> cmb 1 .click res 5 : java.lang.String = 1 : I 'm number one! scala> cmb2.click res6: java.lang.String = 0: I' m number two! scala> cmb 1 .click res 7 : java.lang.String = 2 : I 'm number one! scala> cmb2.click res8: java.lang.String = 1: I' m number two! |
В. Почему есть функции toString , toInt и toList , но нет функции toTuple ?
О. Это основной вопрос, который ведет непосредственно к более продвинутой теме последствий . Есть несколько причин этого. Для начала важно понять, что существует множество типов кортежей, начиная с кортежа с одним элементом (Tuple1) до 22 элементов (Tuple22). Обратите внимание, что когда вы используете (,) для создания кортежа, он неявно вызывает конструктор для соответствующего TupleN правильной арности.
1
2
3
4
5
6
7
8
|
scala> val b = ( 1 , 2 , 3 ) b : (Int, Int, Int) = ( 1 , 2 , 3 ) scala> val c = Tuple 3 ( 1 , 2 , 3 ) c : (Int, Int, Int) = ( 1 , 2 , 3 ) scala> b == c res 4 : Boolean = true |
Учитывая это, очевидно, не имеет смысла иметь функцию toTuple для Seqs (последовательностей), которая длиннее 22. Это означает, что не существует универсального способа иметь, скажем, List или Array, а затем вызывать toTuple для него и ожидать надежного поведения. случаться.
Однако, если вам нужна эта функциональность (даже если она ограничена приведенным выше ограничением максимум в 22 элемента), Scala позволяет вам «добавлять» методы в существующие классы, используя неявные определения. Вы можете найти множество дискуссий о последствиях, выполнив поиск по «scala implicits». Но вот пример, который показывает, как это работает для этого конкретного случая.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
val foo = List( 1 , 2 ) val bar = List( 3 , 4 , 5 ) val baz = List( 6 , 7 , 8 , 9 ) foo.toTuple class TupleAble[X] (elements : Seq[X]) { def toTuple = elements match { case Seq(a) = > Tuple 1 (a) case Seq(a,b) = > (a,b) case Seq(a,b,c) = > (a,b,c) case _ = > throw new RuntimeException( "Sequence too long to be handled by toTuple: " + elements) } } foo.toTuple implicit def seqToTuple[X](x : Seq[X]) = new TupleAble(x) foo.toTuple bar.toTuple baz.toTuple |
Если вы поместите это в Scala REPL, вы увидите, что при первом вызове foo.toTuple появляется ошибка:
1
2
3
4
|
scala> foo.toTuple <console> : 9 : error : value toTuple is not a member of List[Int] foo.toTuple ^ |
Обратите внимание, что класс TupleAble принимает Seq в своем конструкторе, а затем предоставляет метод toTuple , используя этот Seq. Это может сделать это для Seqs с 1, 2 или 3 элементами, и, кроме того, оно выдает исключение. (Конечно, мы могли бы продолжать перечислять больше случаев и использовать до 22 наборов элементов, но это показывает смысл.)
Второй вызов foo.toTuple по- прежнему не работает — и это потому, что foo — это List (разновидность Seq), а для списков нет метода toTuple . Вот тут и появляется неявная функция seqToTuple — как только она объявлена, Scala отмечает, что вы пытаетесь вызвать toTuple для Seq, отмечает, что такой функции для Seqs нет, но видит, что существует неявное преобразование из Seqs в TupleAbles через seqToTuple , и затем он видит, что TupleAble имеет метод toTuple . Основываясь на этом, он компилирует и производит желаемое поведение. Это очень удобная возможность Scala, которая может действительно упростить ваш код, если вы используете его правильно и с осторожностью.
Справка: вопросы студентов о Scala, часть 1 от нашего партнера JCG