Що так важко вказувати / рекурсувати? [зачинено]


20

У небезпеці шкіл java Джоел обговорює свій досвід роботи в Пенні та складність "помилок сегментації". Він каже

[segfault важко, поки ви] "зробите глибокий вдих і дійсно намагаєтесь змусити свій розум працювати на двох різних рівнях абстракції одночасно".

З огляду на перелік поширених причин виникнення segfault, я не розумію, як ми повинні працювати на двох рівнях абстракції.

Чомусь Джоел вважає ці поняття основними для здатності програмістів до абстрактного. Я не хочу занадто припускати. Отже, що так важко вказувати / рекурсувати? Приклади були б непогані.


31
Перестаньте турбуватися про те, що Джоел може подумати про вас. Якщо ви вважаєте рекурсію легкою, це добре. Не всі інші так роблять.
FrustratedWithFormsDesigner

6
Рекурсія проста за визначенням (функція, яка викликає самоврядування), але знати, коли її використовувати та як змусити її працювати, - найважча частина.
JeffO

9
Подайте заявку на роботу в Fog Creek і повідомте нам, як це відбувається. Ми всі дуже зацікавлені у вашій саморекламі.
Джоел Етертон

4
@ P.Brian.Mackey: Ми не розуміємо. Питання насправді нічого не задає. Це кричуща самореклама. Якщо ви хочете дізнатися, що Джоел запитує про покажчики / рекурсії, запитайте його: team@stackoverflow.com
Джоел

19
Дублікат цього питання ?
ozz

Відповіді:


38

Я вперше помітив, що покажчики та рекурсія були важкими в коледжі. Я пройшов пару типових курсів на першому курсі (один - C та Assembler, другий - у схемі). Обидва курси розпочалися з сотень студентів, багато з яких мали багаторічний досвід програмування у середній школі (як правило, в ті часи BASIC та Pascal). Але як тільки введено покажчики в курсі C, і рекурсія була запроваджена в курсі «Схема», величезна кількість студентів - можливо, навіть більшість - були повністю керовані. Це були діти, які раніше писали багато коду і взагалі не мали жодних проблем, але коли вони потрапляли на вказівники та рекурсію, вони також вражали стіну з точки зору своїх пізнавальних здібностей.

Моя гіпотеза полягає в тому, що вказівники та рекурсія однакові, оскільки вони вимагають від вас одночасно тримати рівень абстракції в голові. Існує щось про багаторівневе абстрагування, яке вимагає типу розумової здатності, яку, можливо, у деяких людей ніколи не буде.

  • Указівками "два рівні абстракції" є "дані, адреса даних, адреса адреси даних тощо" або те, що ми традиційно називаємо "значення проти посилання". Непідготовленому студенту дуже важко помітити різницю між адресою x і x .
  • З рекурсією "два рівні абстракції" розуміють, як можливо функція викликати себе. Рекурсивний алгоритм - це те, що люди називають "програмуванням із бажаним мисленням", і дуже-дуже неприродно думати алгоритм в терміні "базовий випадок + індуктивний випадок" замість більш природного "списку кроків, які ви слідуєте для вирішення проблеми. . " Для непідготовленого студента, який дивиться на рекурсивний алгоритм, алгоритм, здається, задає питання .

Я також абсолютно хотів би прийняти, що можна навчати вказівникам та / або рекурсії для кого-небудь ... У мене немає жодних доказів так чи інакше. Я знаю, що емпірично, будучи здатним реально зрозуміти ці два поняття, це дуже, дуже хороший прогноз загальної здатності до програмування, і що в звичайному курсі підготовки бакалаврату ці два поняття є однією з найбільших перешкод.


4
"дуже, дуже неприродно думати про алгоритм з точки зору" базовий випадок + індуктивний випадок "" - Я думаю, це аж ніяк неприродно, це просто те, що діти не навчаються відповідно.
Інго

14
якби це було природно, вам не потрібно було б навчатись. : P
Джоель Спольський

1
Добре питання :), але чи не потрібні нам заняття з математики, логіки, фізики тощо? Цікаво, що мало хто з програмістів має якісь проблеми з синтаксисом мов, але він повний рекурсії.
Інго

1
У моєму університеті перший курс розпочався з функціонального програмування та рекурсії майже одразу, задовго до введення мутації тощо. Я виявив, що деякі студенти без досвіду розуміють рекурсію краще, ніж ті, хто має певний досвід. Однак, саму верхівку класу складали люди з великим досвідом.
Тихон Єлвіс

2
Я думаю, що нездатність зрозуміти покажчики та рекурсію пов'язана з а) загальним рівнем IQ та б) поганою математичною освітою.
Quant_dev

23

Рекурсія - це не просто "функція, яка викликає себе". Ви по-справжньому не оціните, чому рекурсія є складною, поки ви не опинитеся, як намалюєте стек-фрейми, щоб зрозуміти, що пішло не так з вашим рекурсивним аналізатором спуску. Часто у вас будуть взаємно рекурсивні функції (функція A викликає функцію B, яка викликає функцію C, яка може викликати функцію A). Це може бути дуже важко зрозуміти, що пішло не так, коли ви знаходитесь N стекфреймів глибоко у взаємно-рекурсивній серії функцій.

Щодо покажчиків, знову ж таки, концепція покажчиків досить проста: змінна, що зберігає адресу пам'яті. Але знову ж таки, коли щось зіпсується вашою складною структурою даних void**вказівників, які вказують на різні вузли, ви побачите, чому це може стати складним, коли ви намагаєтеся зрозуміти, чому один із ваших покажчиків вказує на адресу сміття.


1
Реалізація рекурсивного гідного аналізатора була тоді, коли я справді відчував, що в мене є деякий захват від рекурсії. Покажчики легко зрозуміти на високому рівні, як ви сказали; не зрозумієте, чому вони складні, поки ви не потрапите в гайки та болти реалізацій, що стосуються покажчиків.
Кріс

Взаємна рекурсія між багатьма функціями по суті однакова goto.
starblue

2
@starblue, не дуже - оскільки кожен стек-кадр створює нові екземпляри локальних змінних.
Чарльз Сальвія

Ти маєш рацію, тільки хвіст рекурсія така сама, як goto.
starblue

3
@wnoise int a() { return b(); }може бути рекурсивним, але це залежить від визначення b. Тож це не так просто, як здається ...
альтернатива

14

Java підтримує покажчики (їх називають посиланнями) і підтримує рекурсію. Тож на поверхні його аргументи здаються безглуздими.

Те, про що він насправді говорить, - це можливість налагодження. Вказівник Java (помилка, посилання) гарантовано вказує на дійсний об'єкт. Вказівник змінного струму не є. І хитрість у програмуванні на С, якщо припустити, що ви не використовуєте такі інструменти, як valgrind , полягає в тому, щоб точно з’ясувати, де ви накрутили вказівник (це рідко є в точці, що знаходиться в стек-трасі).


5
Показники самі по собі є деталлю. Використання посилань на Java не складніше, ніж використання локальних змінних у C. Навіть змішування їх у способі виконання Lisp-реалізацій (атом може бути цілим числом обмеженого розміру, або символом, або покажчиком) не є складним. Це стає складніше, коли мова дозволяє однотипним даним бути локальним або посилатися, з різним синтаксисом і по-справжньому волохатим, коли мова дозволяє арифметикувати вказівниками.
Девід Торнлі

@David - гм, що це стосується моєї відповіді?
Анон

1
Ваш коментар щодо підтримуючих покажчиків Java.
Девід Торнлі

"там, де ви накрутили вказівник (рідко в точці, знайденій у стеці)." Якщо вам пощастило отримати стек-трек.
Омега Кентаври

5
Я згоден з Девідом Торнлі; Java не підтримує покажчики, якщо я не можу зробити вказівник на вказівник на вказівник на вказівник на int. Що, можливо, я думаю, що я міг би зробити, як 4-5 класів, що кожен посилається на щось інше, але це дійсно вказівки чи це потворне вирішення?
альтернатива

12

Проблема з покажчиками та рекурсією полягає не в тому, що їх обов'язково важко зрозуміти, а в тому, що вони навчаються погано, особливо стосовно таких мов, як C або C ++ (головним чином через те, що самі мови викладаються погано). Кожного разу, коли я чую (або читаю) когось, що каже "масив - це лише вказівник", я трохи помираю всередині.

Аналогічно, кожен раз, коли хтось використовує функцію Фібоначчі для ілюстрації рекурсії, я хочу кричати. Це поганий приклад, тому що ітераційну версію не важче написати і вона працює як мінімум так само добре або краще, ніж рекурсивна, і вона не дає реального розуміння того, чому рекурсивне рішення було б корисним чи бажаним. Кукіссор, обхід дерев тощо є набагато кращими прикладами того, чому і як відбувається рекурсія.

Доводиться спілкуватися з покажчиками - це артефакт роботи мовою програмування, яка їх викриває. Покоління програмістів Fortran складали списки та дерева, стеки та черги, не потребуючи виділеного типу вказівника (або динамічного розподілу пам’яті), і я ніколи не чув, щоб хтось звинувачував Fortran у іграшці.


Погодьтесь, у мене були роки / десятиліття Фортран до того, як я бачив фактичні покажчики, тому я вже використовував свій власний спосіб робити те саме, перш ніж мені було надано можливість дозволити lanquage / компілятору зробити це за мене. Я також думаю, що синтаксис C щодо покажчиків / адрес дуже заплутаний, навіть якщо поняття значення, яке зберігається за адресою, дуже просте.
Omega Centauri

якщо у вас є посилання на Quicksort, реалізований у Fortran IV, я б хотів його побачити. Не кажучи про те, що цього неможливо зробити - насправді я впровадив це в BASIC близько 30 років тому, - але мені було б цікаво це побачити.
Anon

Я ніколи не працював у Fortran IV, але я реалізував деякі рекурсивні алгоритми в реалізації VAX / VMS Fortran 77 (був гак, який дозволив вам зберегти ціль goto як спеціальний вид змінної, щоб ви могли писати GOTO target) . Я думаю, що нам довелося будувати власні стеки виконання. Це було досить давно, що я вже не можу згадати деталі.
Джон Боде

8

Існує кілька труднощів з покажчиками:

  1. Збудження Можливість зміни значення об'єкта за допомогою різних імен / змінних.
  2. Не місцевість Можливість змінити значення об'єктів у контексті, відмінному від того, в якому він оголошений (це також відбувається з аргументами, переданими посиланням).
  3. Невідповідність життя Термін служби вказівника може відрізнятися від терміну експлуатації об'єкта, на який він вказує, і це може призвести до недійсних посилань (SEGFAULTS) або сміття.
  4. Арифметика вказівника . Деякі мови програмування дозволяють маніпулювати покажчиками як цілі числа, а це означає, що вказівники можуть вказувати куди завгодно (включаючи найнесподіваніші місця, коли є помилка). Щоб правильно використовувати арифметику вказівника, програміст повинен знати про розміри пам’яті об’єктів, на які вказували, і це щось більше, про що слід подумати.
  5. Тип Casts Можливість передавати покажчик від одного типу до іншого дозволяє перезаписати пам'ять об'єкта, відмінного від призначеного.

Тому програміст повинен продумати більш ретельно, використовуючи покажчики (я не знаю про два рівні абстракції ). Це приклад типових помилок, допущених новачком:

Pair* make_pair(int a, int b)
{
    Pair p;
    p.a = a;
    p.b = b;
    return &p;
}

Зауважте, що такий код, як зазначено вище, цілком розумний для мов, які не мають поняття покажчиків, а скоріше одного з імен (посилань), об'єктів та значень, як функціональних мов програмування, і мов зі збиранням сміття (Java, Python). .

Труднощі з рекурсивними функціями трапляються, коли люди, які не мають достатнього математичного досвіду (де рекурсивність є загальним і потрібним знанням), намагаються підійти до них, думаючи, що функція буде вести себе по-різному, залежно від того, скільки разів вона була викликана раніше . Ця проблема посилюється через те, що рекурсивні функції справді можна створити таким чином, щоб ви мали думати саме так, щоб їх зрозуміти.

Подумайте про рекурсивні функції з передачею вказівників, як у процедурному виконанні Червоно-Чорного дерева, в якому структура даних змінюється на місці; це щось складніше думати, ніж функціональний аналог .

Про це не йдеться, але інше важливе питання, з яким у новачків виникають труднощі, - це паралельність .

Як уже згадували інші, існує додаткова неконцептуальна проблема з деякими конструкціями мови програмування: це те, що навіть якщо ми розуміємо, прості та чесні помилки з цими конструкціями можуть бути надзвичайно важкими для налагодження.


Використання цієї функції поверне дійсний покажчик, але змінна знаходиться в області, що перевищує область, яка викликала функцію, тому вказівник міг (припустимо, буде визнаний недійсним), коли використовується malloc.
праворуч

4
@Radek S: Ні, не буде. Він поверне недійсний покажчик, який у деяких середовищах працює деякий час, поки щось інше не замінить його. (На практиці це буде стек, а не купа. malloc()Це не більше шансів, ніж будь-яка інша функція.)
wnoise

1
@Radeck У прикладі функції вказівник вказує на пам'ять, яку гарантує мова програмування (у цьому випадку C), коли функція повернеться. Таким чином, повернутий вказівник вказує на сміття . Мови зі збиранням сміття підтримують об'єкт живим до тих пір, поки на нього не посилається будь-який контекст.
Апалала

До речі, у Русті є вказівники, але без цих проблем. (коли не в небезпечному контексті)
Сардж Борш

2

Покажчики та рекурсія - це два окремі звірі, і є різні причини, які кваліфікують кожного з них як "важкого".

Загалом, покажчики потребують ментальної моделі, ніж чисте змінне призначення. Коли у мене є змінна вказівник, це просто так: вказівник на інший об’єкт, єдині дані, які він містить, - це адреса пам'яті, на яку він вказує. Так, наприклад, якщо у мене є вказівник int32 і присвоюю йому значення безпосередньо, я не змінюю значення int, я вказую на нову адресу пам'яті (є дуже багато акуратних хитрощів, які ви можете зробити з цим ). Ще цікавіше - мати покажчик на покажчик (саме це відбувається, коли ви передаєте змінну Ref як функцію Параметр у C #, функція може призначити абсолютно інший об’єкт Параметру, і це значення все ще буде в області застосування, коли функція виходи.

Рекурсія має незначний розумовий стрибок при першому навчанні, оскільки ви визначаєте функцію з точки зору самої себе. Це дике поняття, коли ви вперше стикаєтеся з ним, але як тільки ви зрозумієте цю ідею, вона стає другою природою.

Але повернемося до теми, що знаходиться під рукою. Аргумент Джоеля полягає не у вказівках чи рекурсії в собі, а в тому, що студентів віддаляють від того, як комп’ютери дійсно працюють. Це наука в галузі інформатики. Існує чітка різниця між навчанням програмі та навчанням того, як працюють програми. Я не думаю, що це вже справа не в тому, що "я навчився цьому таким чином, тому всі повинні навчитися цьому", як він стверджує, що багато програм CS стають прославленими торговими школами.


1

Я даю П. Брайану +1, тому що я відчуваю, що він це робить: рекурсія - це настільки фундаментальна концепція, що той, хто має найменші труднощі з цим, повинен краще розглянути питання про роботу в mac donalds, але тоді, навіть є рекурсія:

make a burger:
   put a cold burger on the grill
   wait
   flip
   wait
   hand the fried burger over to the service personel
   unless its end of shift: make a burger

Безумовно, відсутність розуміння стосується і наших шкіл. Тут слід ввести натуральні числа, такі як Пеано, Дедекінд та Фреге, так що ми не матимемо стільки труднощів згодом.


6
Це хвороба, яка, певно, петля.
Майкл К

6
Вибачте, для мене петля - це, мабуть, хвостова рекурсія :)
Ingo

3
@Ingo: :) Функціональний фанатик!
Майкл К

1
@Michael - так, правда !, але я думаю, що можна зробити так, що рекурсія є найбільш фундаментальною концепцією.
Інго

@Ingo: Дійсно, можна (ваш приклад це добре демонструє). Однак чомусь людям важко з цим в програмуванні - ми, здається, хочемо цього додатково goto topз якихось причин IME.
Майкл К

1

Я не погоджуюся з Джоелем, що проблема полягає в тому, що мислення на кількох рівнях абстрагування саме по собі, я думаю, що це більше, ніж покажчики та рекурсія - це два хороших приклади проблем, які потребують зміни в ментальній моделі, яку люди мають, як працюють програми.

Покажчики, я думаю, простіший випадок для ілюстрації. Для роботи з покажчиками необхідна ментальна модель виконання програми, яка враховує спосіб фактичної роботи програм з адресами та даними пам'яті. Мій досвід такий, що часто програмісти навіть не думали про це, перш ніж дізнатися про покажчики. Навіть якщо вони знають це в абстрактному розумінні, вони не взяли його в свою пізнавальну модель того, як працює програма. Коли вводяться покажчики, це вимагає принципового зміни в тому, як вони думають про те, як працює код.

Рекурсія є проблематичною, оскільки є два концептуальні блоки для розуміння. Перший - на машинному рівні, і подібно до покажчиків його можна подолати, розвиваючи добре розуміння того, як програми насправді зберігаються та виконуються. Інша проблема рекурсії полягає в тому, що люди мають природну схильність намагатися деконструювати рекурсивну проблему в нерекурсивну, що затуманює розуміння рекурсивної функції як гештальту. Це або проблема людей з недостатньою математичною основою, або ментальна модель, яка не пов'язує математичну теорію з розробкою програм.

Справа в тому, що я не думаю, що покажчики та рекурсія - це єдині дві сфери, які є проблематичними для людей, застряглих у недостатній ментальній моделі. Паралелізм, здається, є ще однією областю, до якої деякі люди просто зациклюються і мають труднощі з адаптацією своєї ментальної моделі для врахування. Це просто те, що часто вказівки та рекурсії легко перевірити в інтерв'ю.


1
  DATA    |     CODE
          |
 pointer  |   recursion    SELF REFERENTIAL
----------+---------------------------------
 objects  |   macro        SELF MODIFYING
          |
          |

Поняття самореференційних даних та коду лежить в основі визначення покажчиків та рекурсії відповідно. На жаль, широке опромінення імперативних мов програмування змусило студентів інформатики вважати, що вони повинні розуміти реалізацію через оперативну поведінку їхнього часу виконання, коли їм слід довіряти цю таємницю функціональному аспекту мови. Підсумовування всіх чисел до сотні здається простим питанням, починаючи з одиниці та додаючи її до наступної в послідовності та виконуючи її назад за допомогою кругових самореференційних функцій, здається перекрученим і навіть небезпечним для багатьох, які не звикли до безпеки чисті функції.

Поняття самовиправлення даних і коду лежить в основі визначення об'єктів (тобто розумних даних) і макросів відповідно. Я згадую про них, оскільки їх ще важче зрозуміти, особливо коли очікується оперативне розуміння часу виконання від поєднання всіх чотирьох понять - наприклад, макрос, що генерує набір об'єктів, що реалізує рекурсивний гідний аналізатор за допомогою дерева покажчиків . Замість того, щоб простежити всю операцію стану програми покроково через кожен шар абстракції одразу, імперативні програмісти повинні навчитися вірити, що їх змінні призначаються лише один раз у межах чистих функцій і що повторні виклики тієї ж чистої функції з ті ж самі аргументи завжди дають той самий результат (тобто референсна прозорість), навіть у мові, яка підтримує нечисті функції, як і Java. Бігати по колу після виконання - це безрезультатне починання. Абстракція повинна спрощуватись.


-1

Дуже схожа на відповідь Анона.
Окрім пізнавальних труднощів для новачків, як вказівники, так і рекурсія дуже потужні, і їх можна застосовувати криптовалютно.

Недоліком великої потужності є те, що вони дають вам велику силу, щоб викрутити вашу програму тонко.
Збереження фальшивого значення в звичайну змінну є досить поганим, але зберігання чогось фіктивного вказівника може спричинити всілякі затримки катастрофічних речей.
І що ще гірше, ці ефекти можуть змінитися, коли ви намагаєтеся діагностувати / налагоджувати, що є причиною химерної поведінки програми. Але якщо щось зроблено тонко неправильно, може бути важко зрозуміти, що відбувається.

Аналогічно з рекурсією. Це може бути дуже потужним способом впорядкування складних речей - шляхом введення хитрощів у приховану структуру даних (стек).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.