как устроена область видимости?

203
08 сентября 2018, 07:40

мучает вопрос, как внутри работает область видимости в различных языках? Ну или хотя бы интересно было бы узнать общую концепцию. как например компилятор си (/c++/c#/java/...) позволяет в разных методах использовать одно и тоже название переменной? меняет "название" переменной на стадии компиляции в зависимости от его родителя? а если два потока одновременно выполняясь читают одну и ту же переменную? ведь потоки могут вызываться в неизвестном для компилятора количестве динамически в реалтайме, так же как и динамически могут создаться в ООП объекты имея внутри одинаковые названия переменных. Чтение исходников JVM и cpython не особо помог

Answer 1

Чтобы узнать общую концепцию, стоит почитать книгу дракона. Здесь попробую осветить в общих чертах работу компилятора Java. Компиляция исходника в байткод проходит в несколько стадий:

  1. Parse. Читаются исходные файлы, затем токены преобразуются в элементы абстрактного синтаксического дерева.
  2. Enter. Строится таблица символов путём обхода дерева.
  3. Process (annotations). Если в компиляции участвуют процессоры аннотаций, то происходит обработка аннотаций, затем, если сгенерировались новые классы, компилятор вновь переходит к стадии Parse.
  4. Attribute. Синтаксические деревья помечаются атрибутами. На этом этапе происходит разрешение имён, проверка типов, и оптимизация свёртывания констант.
  5. Flow. Выполняется анализ потока данных. Проверяется правильность присвоений, цепочек вызовов, а также достижимость кода.
  6. Desugar. Происходит преобразование АСД с целью убрать синтаксический сахар. На этой стадии перечисления преобразуются в специальный класс, а вместо лямбд и ссылок на методы подставляются фабрики и прочие классы из пакета java.lang.invoke.
  7. Generate. Генерируются class-файлы.

Интересующий вас процесс происходит на стадиях 2, 4 и 5. Центральный элемент этого процесса - таблица символов. Именно по ней компилятор ориентируется при определении, что и где видимо.

Интересный момент в том, что области видимости переменных в Java существуют и имеют значение только на этапе компиляции, в байткоде этой информации уже не будет. Например попытка скомпилировать такой код

public static void test() {
    int a = 1;
    {
        int b = 2;
    }
    System.out.println(a);
    System.out.println(b);
}

выдаст ошибку "cannot find symbol". Перенесём вывод переменной b внутрь блока и скомпилируем код с отладочной информацией javac -g Example.java. А теперь заглянем в байткод javap -c -v -p Example.class

public static void test();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=2, args_size=0
       0: iconst_1
       1: istore_0
       2: iconst_2
       3: istore_1
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: iload_1
       8: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      11: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      14: iload_0
      15: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      18: return
    LineNumberTable:
      line 52: 0
      line 55: 2
      line 56: 4
      line 59: 11
      line 60: 18
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          4       7     1     b   I
          2      17     0     a   I

Благодаря ключу -g в самом конце листинга можно увидеть таблицу локальных переменных. JVM эту информацию не использует, для неё есть только слоты - порядковые номера переменных, а об их именах и видимости она ничего не знает. Если опкод iload_0 по 14-му смещению поменять на опкод iload_1, виртуальная машина без зазрения совести выведет в консоль значение "невидимой" переменной b.

О полях в байткоде сохраняется чуть больше информации, чем о переменных. Эту информацию компилятор записывает в пул констант - область class-файла до его загрузки и область MetaSpace после загрузки. Каждому полю в пуле соответствует одна запись типа Fieldref хранящая разделённые точкой указатели на другие области в пуле. Значение до точки указывает на класс, к которому относится поле, а значение после точки указывает на запись типа NameAndType. Запись NameAndType в свою очередь тоже хранит два указателя, но уже разделённые двоеточием. Первый указывает на запись хранящую имя поля, а второй на запись хранящую тип. Как говорится, лучше один раз увидеть, чем 100 раз услышать. Скомпилируем, а потом заглянем внутрь этого простого класса:

public class Example {
    private int x = 42;
    private int getX() {
        return x;
    }
    public static void main(String[] args) {
        Example obj = new Example();
        obj.getX();
    }
}

В самом начале мы увидим пул констант:

Constant pool:
   #1 = Methodref          #6.#25         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#26         // Example.x:I
   #3 = Class              #27            // Example
   #4 = Methodref          #3.#25         // Example."<init>":()V
   #5 = Methodref          #3.#28         // Example.getX:()I
   #6 = Class              #29            // java/lang/Object
   #7 = Utf8               x
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LExample;
  #16 = Utf8               getX
  #17 = Utf8               ()I
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               obj
  #23 = Utf8               SourceFile
  #24 = Utf8               Example.java
  #25 = NameAndType        #9:#10         // "<init>":()V
  #26 = NameAndType        #7:#8          // x:I
  #27 = Utf8               Example
  #28 = NameAndType        #16:#17        // getX:()I
  #29 = Utf8               java/lang/Object

Второй элемент пула констант - это информация о поле x. Чуть ниже в листинге можно найти, как JVM понимает из какого именно объекта нужно взять поле:

0: new           #3                  // class Example
3: dup
4: invokespecial #4                  // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #5                  // Method getX:()I
12: pop
13: return

По смещению 8 располагается опкод aload_1 загружающий в стек ссылку на объект obj. Следом за ним происходит вызов метода getX.

0: aload_0
1: getfield      #2                  // Field x:I
4: ireturn

Первое, что делает метод getX - загружает переданную при вызове ссылку на объект в свой кадр стека. Следующий опкод getfiled использует эту ссылку, чтобы извлечь значение поля x именно из того объекта, на которой она указывает. То есть в псевдокоде получение значения x объекта obj выглядит примерно так:

void main() {
    Example obj = new Example();
    Example.getX(obj);
}
int getX(this) {
    return this.x;
}
READ ALSO
Telegram создание бота через реального юзера [закрыт]

Telegram создание бота через реального юзера [закрыт]

Как через https://githubcom/rubenlagus/TelegramApi создать бота который будет обрабатывать сообщения юзеров ?

215
Реализация конечного автомата

Реализация конечного автомата

Знаю про конечные автоматы только какие-то основы, наподобие определения, сути и тд Подскажите, где подробно почитать про их реализацию? (желательно...

217
Можно ли запустить метод start не используя launch?

Можно ли запустить метод start не используя launch?

Для запуска метода start() в javafx нужно в main() написать Applicationlaunch(args)

158
Что означает cлэш в css

Что означает cлэш в css

Недавно при просмотре урока по верстке лендинга заметил вот такую запись:

187