Подтвердить что ты не робот

В чем причина наличия только одного возвращаемого значения в С++ и Java?

Обычно я просматриваю этот факт и просто принимаю его как "это то, как колесо вращается", но сегодня я задаюсь вопросом, откуда это происходит: Почему функция имеет только одно значение возврата (ссылки)?

Почему не может, было бы сложно или неразумно, чтобы функция возвращала несколько значений? Это из-за объектов и как вы должны ожидать, что пакет данных всегда должен быть возвращен в виде ссылки на объект?

Если вы видите объявленную функцию как контракт, в которой указывается, как можно вызвать функцию, какие параметры она должна принимать и какой тип возвращаемого значения она имеет, то я не вижу логики, которая имеет только одно возвращаемое значение, поскольку вы можете применить ту же логику по-другому (таким образом, создавая контракт для более чем одного возвращаемого значения).

Единственная причина, основанная на логике, которую я вижу, заключается в том, что если функция возвращает более 1 вещи, она также должна манипулировать более чем 1 вещью по более чем одной причине, которая противоречит философии, что функция должна выполнять только одно. Если эта вещь управляет объектом и возвращает другой объект, это имеет смысл, и вы можете вернуть ссылочное значение с помощью управляемого объекта.

Итак, почему именно это ограничение существует?

4b9b3361

Ответ 1

Всякий раз, когда у вас есть несколько значений x1... xN, вы тривиально имеете кортеж (x1, ..., xN), который является единственным (хотя и составным) значением. Если у вас нет кортежей на вашем языке, используйте любой тип агрегата (struct, class, массивы, другие коллекции) по своему усмотрению. Поэтому с этой точки зрения возвращение "нескольких" значений и возврат одного значения полностью эквивалентно.

Это просто означает, что вам нужен только один, но зачем опускать другого?

Во-первых, вам нужны агрегированные типы в любом случае, поэтому выбор заключается в том, что они имеют или имеют только совокупные типы. Во-вторых, если функция может возвращать "несколько значений", вы сталкиваетесь с семантической загадкой: внезапно выражение не оценивает значение (которое мы ранее определили очень конкретно), это новая, другая категория вещей, называемая "несколько значений". Теперь, каков статический тип этого результата? Что может делать программа и не делать с ней? Что это означает в терминах реализации?

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

Ответ 2

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

Например, в С++ std::set::insert необходимо вернуть два значения - итератор и bool, чтобы указать, является ли итератором какой-то старый элемент или элемент, который мы только что вставили. Решением было вернуть std::pair:

pair<iterator,bool> insert (const value_type& val);

Второе соображение чисто практично: работа с множественным возвращаемым значением в выражениях типа

int x = f(y);

прост и интуитивен, когда имеется только одно возвращаемое значение. Попытка вернуть несколько значений, как в

int x, y = f(w, z);

будет сложно разобрать, потому что читатель должен проверить тип возврата f и посмотреть, возвращает ли он int или два int s.

Я даже не касаюсь ситуации, когда типы данных возвращенных элементов не совпадают! Синтаксис быстро станет кошмаром, без сопоставимого усиления выразительности власти.

Ответ 3

Я хочу, чтобы люди перестали интерпретировать "Я не могу делать X в языке Y", поскольку "язык Y имеет ограничение, которое мешает мне делать X". Иногда это на самом деле просто правда. например, модификаторы С++ 'const в основном просто запрещают вам изменять мутации значения таким образом, чтобы синтаксис мог быть хорошо выражен, но просто не считается разумным, если вы хотите, чтобы ваша программа вела себя определенным предсказуемым образом.

OTOH, для большинства функций X вы можете придумать это, просто не имеет смысла хотеть их на языке. Как насчет того, спросил я, "почему я не могу просто дифференцировать все функции С++? "Для физика или инженера, который ничего не знает о программировании, это не казалось бы надуманной задачей, поскольку эти ребята традиционно используют только 1 конкретный вид в математическом смысле, который всегда можно различать так часто, как вам нравится.

Но для большинства функций в языках программирования совершенно не имеет смысла, концептуально, взять производную. Для программиста аналитические функции могут быть лишь крошечным подмножеством всех функций, просто потому, что идея производных применима только к непрерывным областям (на практике, в основном к векторным пространствам) 2 и, конечно же, это полностью несовместим с побочными эффектами, которые "функционируют" в процедурных и языках OO, разрешено иметь.

Теперь, позволяя возвращать несколько значений, несомненно, выглядит гораздо более мирским, чем позволять дифференцировать функции, но все же существенно меняется, что подразумевается под "функцией". Вы больше не можете просто ссылаться на тип возвращаемого значения функции, вам нужно уточнить, что значит создавать функции с несколькими возвращаемыми значениями, вам нужно придумать какой-то новый синтаксис для указателей на функции и т.д. И т.д.
В отличие от примера дифференциации, все это, вероятно, будет возможно, но должно быть ясно, что он сильно изменит весь язык. Это было бы очень не просто "перестать мешать мне возвращать несколько вещей!"

Некоторые языки, например. Fortran и Matlab, имеют жесткую поддержку нескольких возвращаемых значений в языке. О, мальчик, я могу сказать вам, что это не делает программирование на этих языках приятнее: многие вещи, которые вы можете легко сделать с функциями С++, просто заблокированы, потому что они будут перепутаны с помощью нескольких возвратов!
Другие языки, например. Python, есть альтернатива lightwight: похоже, что вы возвращаете несколько вещей, но на самом деле вы просто возвращаете один кортеж и получаете некоторый синтаксический сахар для сопоставления шаблонов компонентам с отдельными переменными.

Возвращаемые кортежи или что-то подобное, конечно, как говорят другие ответы, разумное решение вашей проблемы, хотя иногда это может быть немного неуклюжим на языках, которые не имеют очень хорошей синтаксической поддержки кортежей. (Но это всего лишь немного безобидного шаблона.)


1 Ну, по крайней мере, они притворяются, что имеют дело с аналитическими функциями. Часто это едва ли приемлемо математически. Действительно, они просто аппроксимируют все аналитическими функциями.

2 Интересно, что на языке с соответствующими алгебраическими типами данных появляется замечательный аналог классической идее дифференцирования совершенно неожиданным образом, фактически не нуждаясь в непрерывных областях. Это все еще очень отличается от, например, "возьмем производную от х 2/(1 + e x)".


Идем дальше: можно утверждать, что функции также должны иметь только один аргумент. Это тоже может быть составным типом, но здесь вам даже не нужны кортежи: там называется Currying – любая функция двух аргументов также может быть выражена как функция одного аргумента, возвращая функцию второго аргумента. Это может показаться немного странным способом мышления, но на самом деле это работает очень хорошо. Haskell делает это, следовательно, и многие считают его одной из сумасшедших вещей в отношении языка, но это позволяет вам выражать некоторые операции более разумно, чем это возможно в большинстве других языков, в то время как синтаксис на самом деле очень прост.

Ответ 4

Это чисто исторический. Так работают математические функции. Более того, некоторые языки (довольно много на самом деле) позволяют возвращать более одного значения - обычно используя объект кортежа.

Идея о том, что было бы неплохо вернуть более одного объекта, опаздывала. Почему было выбрано, чтобы сделать это через объекты, а не синтаксис языка? Поскольку программирование OO использовалось в дикой природе и общепринято.

Семантически оба способа имеют одно и то же значение - "вернуть больше одного". Поэтому добавление такой функциональности было бы эквивалентно поддержке другого синтаксиса для того же значения. Наличие двух способов сделать одно не очень приветствуется в программировании. Программисты задаются вопросом, какой путь выбрать, некоторые знают только один метод, а разработчикам компилятора придется поддерживать два механизма, которые не отличаются по своей функциональности. Это довольно хорошее обоснование. Охамская бритва ценится инструментом. И объекты более полезны.

Другое дело, что ваш аргумент: "объявленная функция как контракт, в которой указывается, как можно вызвать функцию, какие параметры она должна принимать и , которая возвращает тип значения, который она имеет", не очень хорошо на жирной части.

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

Ответ 5

В вашем вопросе не указывается, какую именно функцию вы ищете: синтаксический сахар, который делает его "похожим" на то, что вы возвращаете несколько значений или существенную способность функции возвращать несколько значений.

Если это первый, то нет большого ответа, чтобы дать: языки просто отличаются уровнем синтаксической поддержки, который они обеспечивают для этого. Например, LISP имеет полную поддержку для множественного возврата значения, а многие языки (Scala, Clojure) поддерживают деструктурирующую идиуму связывания, которая очень похожа на то, что вы ищете, но работайте с типизированными кортежами. В общем случае Java плохо работает по синтаксису, поэтому отсутствие поддержки связывания деструктурирования не вызывает удивления.

Что касается последнего; в C, возвращающий тип struct - это именно то, что вы ищете. Структура будет помещена в стек стека и будет содержать произвольное количество значений. Другие языки, которые не позволяют вам контролировать размещение стека/кучи на таком уровне детализации, все же позволяют вам возвращать указатель на такую ​​структуру.

Ответ 6

Я никогда раньше не работал над внутренними компонентами компилятора, но термин обычно ссылается на двоичное отношение, которое имеет уникальное отображение для каждого элемента в нем (набор входов) для него codomain ( набор выходов). Теперь эти элементы набора могут быть объединены. Может быть, кто-то, кто более хорошо разбирается в теории компиляторов, может подробно остановиться на этом.

Ответ 7

В математике функция имеет область и кодомен. f:S->T обозначает функцию с областью S и codomain T.

Метод с двумя параметрами и одним возвращаемым значением более или менее соответствует функции f:SxT->U, где x обозначает декартово произведение. Так, например, метод с параметрами типа double и int, который возвращает a boolean, примерно соответствует функции f:DxI->B, где D - это набор всех значений double и т.д. Это делается без объединения двух параметров в кортеж.

Нет причин, по которым мы не могли бы аналогичным образом иметь метод, соответствующий функции формы f:X->YxZ, не возвращая два возвращаемых значения в кортеж.

Например, метод Java

BigInteger[] divideAndRemainder(BigInteger divisor)

может легко иметь подпись

BigInteger q, BigInteger r divideAndRemainder(BigInteger divisor)

Где-то в теле метода должен быть оператор return, который может выглядеть как return q, r;. Для массива вообще не будет причин.

На самом деле это было бы лучше, поскольку вы могли бы вызвать метод, написав

BigInteger q, BigInteger r = bigInt1.divideAndRemainder(bigInt2);

вместо

BigInteger[] arr = bigInt1.divideAndRemainder(bigInt2);
BigInteger q = arr[0];
BigInteger r = arr[1];

Я думаю, причина, по которой это не допускается, заключается в том, что это усложнит язык, если не будет большого спроса на него. Если вы посмотрите в стандартных библиотеках на многих языках и сравните количество методов, которые связывают множество возвращаемых значений с одним объектом, по сравнению с количеством методов, которые имеют более одного аргумента, вы, вероятно, обнаружите, что последнее гораздо более распространено.

Ответ 8

В тот же день некоторые регистры процессора имели особые соглашения, а регистры CPU были ограничены. Таким образом, код сборки обычно возвращает ровно одно значение, например, в AX на 8086.

Написание ассемблерного кода трудоемко, и "C" пришел, чтобы облегчить некоторую нагрузку, в то же время позволяя разработчику добиться одинаковой производительности кода сборки.

Чтобы подчеркнуть эту точку, рассмотрим этот код "C" :

int foo(void)
{
    // no return statement; default register used
}

В этом случае функция foo вернет значение в регистре "возвращаемого значения" для типа ЦП, например EAX на 32-разрядном процессоре x86.

"С++" взаимодействует с "C" и не вводит вторую форму оператора return.

Ответ 9

Функции - это просто подпрограммы, которые оставляют что-то в стеке. Если то, что осталось в стеке, не предсказуемо, теперь у вас должен быть какой-то метод, предсказывающий, что все осталось в стеке. Это создает накладные расходы. Мы решили просто указать на то, что может быть структурой данных многих вещей или просто быть int.

Математик скажет вам, что функция связана с привязкой каждого аргумента (входного значения) к соответствующему выходному значению, но для ученого-компьютер функция - это контракт о состоянии, в котором стек будет оставлен, когда он вернется.

Ответ 10

Я понимаю проблему. Скажем, вы циклически просматриваете коллекцию данных, и вам необходимо отфильтровать данные по некоторому аспекту. В качестве побочного эффекта этих данных перемещения вы узнаете еще одну интересную функцию, которую вы хотели бы иметь, но этот результат не соответствует запрошенному результату функции.

Пример: вы просматриваете весь свой код, чтобы узнать, какие функции вызывают с помощью каких функций. При перемещении вы можете найти в качестве побочных эффектов функции, которые никогда не используются/призваны. Вы будете находить эти функции только при обходе всех функций, это не результат вызова, строго говоря не связанный с результатом, но очень интересная информация для извлечения. Что вы должны сделать? Напишите другую функцию, выполняющую ту же функциональность, но теперь с другим результатом? Создание отдельного объекта, сохраняющего оба результата? Сохранять результат в новой переменной для объекта? Или есть функция с несколькими результатами?

Эти побочные эффекты непредсказуемы с точки зрения разработки языка программирования. Вы действительно не можете создать язык по непредсказуемым требованиям. И как отвечать на него следующим функциям? Для того, чтобы поддерживать функции, как можно слабо связанные друг с другом, насколько это возможно, является хорошей практикой, чтобы как можно проще сохранить результат функций. Функциональный дизайн должен применять Закон Деметры. Предоставление функций имеет результат несвязанных результатов, что делает невозможным запись слабосвязанного кода. Это может стать настоящим кошмаром, если вам нужно изменить цепочку функций, потому что первая функция с несколькими выходами падает на один из результатов - или добавляет к ней новую.

Мое решение этой проблемы заключалось в том, чтобы дважды написать код. Один раз, чтобы найти все отношения между функциями, а другой - найти все функции, которые никогда не вызывались. Таким образом, существует четкое разделение между содержимым функции и остальной частью приложения. Я просто притворился, что не знаю, что я написал эту функцию раньше.

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