【问题标题】:Avoiding Side Effects in C避免 C 中的副作用
【发布时间】:2021-11-16 14:44:05
【问题描述】:

这个问题特别适用于数组。

在许多语言中,我会这样做:

    #This is in python for simplicity:

    def increment(mylist):
        for i in mylist:
            i += 1;
            
        return mylist
    

    mylist = {0,1,2,3}
    mylist = increment(mylist)

我尝试了几种在 C 中返回数组的方法,但都没有像上面那样工作。似乎 C 根本不打算以这种方式工作。相反,我必须这样做:

#include <stdio.h>

increment(int *myarray, int size) {
    for(int i = 0; i < size; i++){
        myarray[i] += 1;
        
    }
}

int main(){
    int myarray[4] = {0,1,2,3};
    increment(myarray, 4);

}

不用说,C 函数会改变数组的状态,因此是一个副作用函数。有充分的理由避免这种情况(这不是本问题的主题)。

有没有办法在 C 中避免这些类型的副作用?

【问题讨论】:

  • 两种方式。要么将数组的副本发送到函数参数,要么将数组参数类型声明为const *

标签: arrays c functional-programming state side-effects


【解决方案1】:

首先,在python中{0,1,2,3}不是一个列表而是一个集合。

与您在 C 中所做的更直接等效的 python 代码是:

def increment(mylist):
    for i in range(len(mylist)):
        mylist[i] += 1;
        
    return mylist


mylist = [0,1,2,3]
mylist = increment(mylist)

在这种情况下,python 中的列表也会产生副作用。 那是因为传递数组最常见的方式是通过引用(或 C 中的指针)

一个更接近你在 python 代码中所做的 C 代码是:

void increment(int *myarray, int size) {
    for(int i = 0; i < size; i++){
        int v = myarray[i]; // copy of the array value here
        v += 1;
    }
}

int main(){
    int myarray[4] = {0,1,2,3};
    increment(myarray, 4);
}

在这种情况下,数组也没有副作用,因为我只是在使用之前复制了数组值。

如果您想避免副作用,一般规则是您必须复制您的数组或单个数组值。

编辑:你可能想要在你的 python 函数中做的是

def increment(mylist):
    mylist = list(mylist) # copy array
    for i in range(len(mylist)):
        mylist[i] += 1
    return mylist

【讨论】:

  • 我需要能够返回结果
  • 您在 python 中的增量函数对 mylist 没有任何副作用,因此您返回的值仍然与调用开始时完全相同...,您可能错过了?在 C 中动态创建新数组的方法在 @lundin 的回答中得到了很好的展示
  • 哦,对了,应该是 mylist[i]=
  • 我编辑了我的答案以添加一个可能与您想在 python 中执行的操作相匹配的函数
【解决方案2】:

改变数组的内容在 C 语言中总是一个“副作用”,正如正式定义所言。如果您正在寻找一种方法来使数组等不可变,例如只读并且总是在操作时创建一个新对象,那么也有办法做到这一点。

您必须注意,这通常涉及数据内容的“硬拷贝”,因此会产生执行开销。如果您不想这样做,C 可以让您选择不要那么低效。但是如果你想要它,那么更灵活的选择是动态分配。像这样的:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int* increment(const int* myarray, int size) 
{
  int* new_obj = malloc(sizeof(int[size]));
  for(int i=0; i<size; i++)
  {
    new_obj[i] = myarray[i] + 1;
  }
  return new_obj;
}

int main (void)
{
  int* myarray = malloc(sizeof(int[4]));
  memcpy(myarray, (int[]){0,1,2,3}, sizeof(int[4]));
  for(int i=0; i<4; i++)
  {
    printf("%d ", myarray[i]);
  }
  puts("");
  
  int* another_array = increment(myarray, 4);
  free(myarray);
  
  for(int i=0; i<4; i++)
  {
    printf("%d ", another_array[i]);
  }

  free(another_array);
}

请注意,这比就地修改原始数组要慢得多。堆分配和数据复制都比较慢。

您可以在 C 中创建“糟糕的 API”函数,例如

int* increment(int *myarray, int size) {
    for(int i = 0; i < size; i++){
        myarray[i] += 1;
        
    }
    return myarray;
}

这将返回一个指向与传递的相同数组的指针。这是一个糟糕的 API,因为它令人困惑,尽管一些 C 标准函数就是这样设计的(strcpy 等)。为了使用这个函数,你需要一个指向数组第一个元素的指针,而不是数组本身。

【讨论】:

  • 它可能更复杂且不符合 Cish,但 a=fn(a) 至少对效果很明确
【解决方案3】:

在 C 代码中,您将指针传递给数组的第一个元素,而数组则保留在内存中。你可以做的是创建一个新数组,然后返回一个指向它的指针。但是,要小心。如果你创建一个auto数组(在栈上创建),它只会存在于函数内部,所以返回的指针会指向垃圾内存。

int* increment(int *myarray, int size) {
    int tempArray[size]; //only exists inside of the function.
    for(int i = 0; i < size; i++){
        tempArray[i] = myarray[i] + 1;
    }
    return tempArray; //don't do this, tempArray will not exist outside of this function.
}

您可以改用malloc 函数,它使用堆内存,并且也存在于函数之外。 (您需要包含 stdlib.h)

#include <stdio.h>
#include <stdlib.h>

int* increment(int *myarray, int size) {
    int* tempArray = malloc(size*sizeof(int)); //exists globally
    for(int i = 0; i < size; i++){
        tempArray[i] = myarray[i] + 1;
    }
    return tempArray;
}

int main(){
    int myarray[4] = {0,1,2,3};
    int* newarray = increment(myarray, 4);
    //use the newarray - myarray stays the same.
    free(newarray); //don't forget to free when you no longer need it
}

【讨论】:

  • 错了,所有函数参数在python中都是通过引用传递的,没有复制
  • 谢谢,看来我对python还不够了解。我编辑了答案。
  • 变量在 python 中的工作方式与在 C 中的工作方式不同,基本上所有变量都包含对值的引用。 OP python代码中的增量函数实际上没有按预期工作(参数由 referenced 传递,并按原样返回,i 只是对“数组”值的引用,但i+=1 对因为整数是不可变的值,所以这只是创建一个新的整数值并在i 变量中分配对该值的引用,该变量会在下一次循环迭代中立即被覆盖。
【解决方案4】:

注意,在 python 中对象是通过引用传递的。

def increment(mylist):
    // mylist is a local reference to the original array
    for i in mylist:
        i += 1;   // i is a local value: nothing is changed in mylist!
        
    return mylist  // returns a reference to the original (and unchanged...) array

应该如何改变原始列表:

def increment(mylist):
    for i in range(len(mylist)):
        mylist[i] += 1
    // returning mylist is optional since the caller's list has been modified

这在 C 中是完全等价的

int *increment(int array[], int size) {
    for (int i=0; i<size; ++i) {
        array[i] += 1;
    }
    return array;
}

但是您可以通过这种方式在 Python 中构建并返回一个全新的列表:

def increment(mylist):
    return [i + 1 for i in mylist]

这在 C 语言中做不到。惯用的方法是让调用者提供数组和大小(如上)或返回动态分配的数组:

int *increment(int array[], int size) {
    int *new_array = malloc(size * sizeof(int));
    for (int i=0; i<size; ++i) {
        new_array[i] = array[i] + 1;
    }
    return new_array;
}

并让调用者通过转让所有权释放返回的数组。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-30
    • 2020-05-05
    • 2013-10-21
    • 1970-01-01
    相关资源
    最近更新 更多